Complete source 6 engineering workflows and preserve release validation evidence
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
//! Independent oracle for the OA communication / SA boundary direction seam.
|
||||
use oa_core::{
|
||||
transitions::*,
|
||||
validation::{self, MessageEndpointResolution as Resolution},
|
||||
*,
|
||||
};
|
||||
|
||||
fn line(p: &mut Project, scenario: Id, owner: Id) -> Id {
|
||||
let id = p
|
||||
.add_element(Kind::Lifeline, Layer::Sa, "Line", Some(scenario))
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
||||
represented: Some(owner),
|
||||
};
|
||||
id
|
||||
}
|
||||
fn message(
|
||||
exchange: Id,
|
||||
sender: Id,
|
||||
receiver: Id,
|
||||
kind: MessageKind,
|
||||
reply: Option<Id>,
|
||||
) -> OccurrenceBody {
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(exchange),
|
||||
sender: Some(sender),
|
||||
receiver: Some(receiver),
|
||||
message_kind: kind,
|
||||
reply_to: reply,
|
||||
values: vec![],
|
||||
}
|
||||
}
|
||||
fn occurrence(p: &mut Project, scenario: Id, body: OccurrenceBody) -> Id {
|
||||
let id = p
|
||||
.add_element(Kind::Occurrence, Layer::Sa, "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
|
||||
}
|
||||
fn boundary_fixture() -> (Project, Id, Id, Id, Id) {
|
||||
let mut p = Project::empty();
|
||||
let oa = p
|
||||
.add_element(Kind::Entity, Layer::Oa, "Team", None)
|
||||
.unwrap();
|
||||
let ob = p
|
||||
.add_element(Kind::Actor, Layer::Oa, "Control", None)
|
||||
.unwrap();
|
||||
let sa = p
|
||||
.add_element(Kind::System, Layer::Sa, "Radio", None)
|
||||
.unwrap();
|
||||
let sb = p
|
||||
.add_element(Kind::SystemActor, Layer::Sa, "Radio control", None)
|
||||
.unwrap();
|
||||
let mean = p.relate(RelationKind::CommunicationMean, "Channel", oa, ob);
|
||||
let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![mean]);
|
||||
request.mappings = vec![
|
||||
TransitionMapping {
|
||||
source: oa,
|
||||
target: Some(sa),
|
||||
target_kind: None,
|
||||
},
|
||||
TransitionMapping {
|
||||
source: ob,
|
||||
target: Some(sb),
|
||||
target_kind: None,
|
||||
},
|
||||
];
|
||||
let port = |alias: &str| CommunicationPortChoice::Create {
|
||||
id: Id::new_v4(),
|
||||
alias: alias.into(),
|
||||
name: alias.into(),
|
||||
description: String::new(),
|
||||
kind: ComponentPortKind::Standard,
|
||||
orientation: ComponentPortOrientation::Unset,
|
||||
};
|
||||
request
|
||||
.communication_endpoints
|
||||
.push(CommunicationEndpointDecision {
|
||||
source: mean,
|
||||
source_port: port("radio_a"),
|
||||
target_port: port("radio_b"),
|
||||
});
|
||||
let plan = plan_transition(&p, &request).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
p = apply_transition(&p, &plan).unwrap();
|
||||
let boundary = p.traces.values().find(|t| t.oa == mean).unwrap().sa;
|
||||
let scenario = p
|
||||
.add_element(Kind::Scenario, Layer::Sa, "Reverse request", None)
|
||||
.unwrap();
|
||||
if let Some(SharedBody::Scenario { viewpoint, .. }) =
|
||||
p.elements.get_mut(&scenario).unwrap().shared_mut()
|
||||
{
|
||||
*viewpoint = ScenarioViewpoint::BoundaryInterface;
|
||||
}
|
||||
let a = line(&mut p, scenario, sa);
|
||||
let b = line(&mut p, scenario, sb);
|
||||
(p, boundary, scenario, a, b)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapped_unoriented_boundary_resolves_reverse_call_and_reply_from_actual_binding() {
|
||||
let (mut p, boundary, scenario, a, b) = boundary_fixture();
|
||||
let (source, target) = p.relations[&boundary].body.endpoints();
|
||||
let call = message(boundary, b, a, MessageKind::Call, None);
|
||||
assert_eq!(
|
||||
validation::resolve_message_endpoints(&p, &call),
|
||||
Resolution::Resolved {
|
||||
source: target,
|
||||
target: source
|
||||
}
|
||||
);
|
||||
let call = occurrence(&mut p, scenario, call);
|
||||
let reply = message(boundary, a, b, MessageKind::Reply, Some(call));
|
||||
assert_eq!(
|
||||
validation::resolve_message_endpoints(&p, &reply),
|
||||
Resolution::Resolved { source, target }
|
||||
);
|
||||
occurrence(&mut p, scenario, reply);
|
||||
let errors: Vec<_> = validation::validate(&p)
|
||||
.into_iter()
|
||||
.filter(|i| i.severity == validation::Severity::Error)
|
||||
.collect();
|
||||
assert!(errors.is_empty(), "{errors:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unoriented_flow_boundary_honors_ordinary_direction_but_preserves_correlated_reply_exception() {
|
||||
let (mut p, boundary, scenario, a, b) = boundary_fixture();
|
||||
let (source, target) = p.relations[&boundary].body.endpoints();
|
||||
for (port, orientation) in [
|
||||
(source, ComponentPortOrientation::Out),
|
||||
(target, ComponentPortOrientation::In),
|
||||
] {
|
||||
if let Some(SharedBody::Port { contract, .. }) =
|
||||
p.elements.get_mut(&port).unwrap().shared_mut()
|
||||
{
|
||||
*contract = PortContract::Component {
|
||||
kind: ComponentPortKind::Flow,
|
||||
orientation,
|
||||
};
|
||||
}
|
||||
}
|
||||
let reverse = message(boundary, b, a, MessageKind::Call, None);
|
||||
assert_eq!(
|
||||
validation::resolve_message_endpoints(&p, &reverse),
|
||||
Resolution::Conflict
|
||||
);
|
||||
let forward = message(boundary, a, b, MessageKind::Call, None);
|
||||
assert_eq!(
|
||||
validation::resolve_message_endpoints(&p, &forward),
|
||||
Resolution::Resolved { source, target }
|
||||
);
|
||||
let call = occurrence(&mut p, scenario, forward);
|
||||
let reply = message(boundary, b, a, MessageKind::Reply, Some(call));
|
||||
assert_eq!(
|
||||
validation::resolve_message_endpoints(&p, &reply),
|
||||
Resolution::Resolved {
|
||||
source: target,
|
||||
target: source
|
||||
}
|
||||
);
|
||||
occurrence(&mut p, scenario, reply);
|
||||
let errors: Vec<_> = validation::validate(&p)
|
||||
.into_iter()
|
||||
.filter(|i| i.severity == validation::Severity::Error)
|
||||
.collect();
|
||||
assert!(errors.is_empty(), "{errors:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reverse_boundary_scenario_transition_keeps_call_orientation_and_reply_identity() {
|
||||
let (mut p, boundary, scenario, a, b) = boundary_fixture();
|
||||
let mean = p.traces.values().find(|t| t.sa == boundary).unwrap().oa;
|
||||
let call = message(boundary, b, a, MessageKind::Call, None);
|
||||
let call = occurrence(&mut p, scenario, call);
|
||||
let reply = message(boundary, a, b, MessageKind::Reply, Some(call));
|
||||
let reply = occurrence(&mut p, scenario, reply);
|
||||
let owner = |port| p.elements[&port].parent.unwrap();
|
||||
let mut request = TransitionRequest::new(TransitionDirection::SaToOa, vec![scenario]);
|
||||
request.scope = DependencyScope::Subtree;
|
||||
request.mappings = [
|
||||
(boundary, mean),
|
||||
(
|
||||
owner(p.relations[&boundary].source()),
|
||||
p.relations[&mean].source(),
|
||||
),
|
||||
(
|
||||
owner(p.relations[&boundary].target()),
|
||||
p.relations[&mean].target(),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(source, target)| TransitionMapping {
|
||||
source,
|
||||
target: Some(target),
|
||||
target_kind: None,
|
||||
})
|
||||
.collect();
|
||||
// The explicit participant counterparts intentionally use different names.
|
||||
// Review those prose differences without changing the endpoint decision.
|
||||
request.resolutions = request
|
||||
.mappings
|
||||
.iter()
|
||||
.map(|mapping| FieldResolution {
|
||||
source: mapping.source,
|
||||
field: TransitionField::Name,
|
||||
decision: FieldDecision::KeepTarget,
|
||||
})
|
||||
.collect();
|
||||
let plan = plan_transition(&p, &request).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
let q = apply_transition(&p, &plan).unwrap();
|
||||
let mapped = |source| q.traces.values().find(|t| t.sa == source).unwrap().oa;
|
||||
let body = |id| match q.elements[&id].shared().unwrap() {
|
||||
SharedBody::Occurrence { body, .. } => body,
|
||||
_ => panic!("expected occurrence"),
|
||||
};
|
||||
let mapped_call = mapped(call);
|
||||
let mapped_reply = mapped(reply);
|
||||
assert_eq!(
|
||||
validation::resolve_message_endpoints(&q, body(mapped_call)),
|
||||
Resolution::Resolved {
|
||||
source: q.relations[&mean].target(),
|
||||
target: q.relations[&mean].source(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
validation::resolve_message_endpoints(&q, body(mapped_reply)),
|
||||
Resolution::Resolved {
|
||||
source: q.relations[&mean].source(),
|
||||
target: q.relations[&mean].target(),
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
matches!(body(mapped_reply), OccurrenceBody::Exchange { reply_to: Some(id), .. } if *id == mapped_call)
|
||||
);
|
||||
assert_eq!(q.elements[&scenario], p.elements[&scenario]);
|
||||
assert_eq!(q.elements[&call], p.elements[&call]);
|
||||
assert_eq!(q.elements[&reply], p.elements[&reply]);
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
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())));
|
||||
}
|
||||
@@ -339,6 +339,30 @@ project mission "Harbor safety — 港 🌊" {
|
||||
component_port,
|
||||
function_port,
|
||||
);
|
||||
let participants: Vec<_> = p
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| matches!(e.kind(), Kind::Entity | Kind::Actor))
|
||||
.map(|e| e.id)
|
||||
.collect();
|
||||
let means = p.relate(
|
||||
RelationKind::CommunicationMean,
|
||||
"Operational communication",
|
||||
participants[0],
|
||||
participants[1],
|
||||
);
|
||||
let category = p
|
||||
.add_element(
|
||||
Kind::ExchangeCategory,
|
||||
Layer::Oa,
|
||||
"Operational exchange category",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&category).unwrap().shared_mut().unwrap() = SharedBody::ExchangeCategory {
|
||||
family: oa_core::view_options::ExchangeFamily::Participant,
|
||||
exchanges: vec![means],
|
||||
};
|
||||
p
|
||||
}
|
||||
|
||||
@@ -491,7 +515,7 @@ fn authoring_defaults_and_forward_references_remain_strict() {
|
||||
#[test]
|
||||
fn typed_unknown_fields_enums_duplicates_and_reference_spelling_are_rejected() {
|
||||
for (source, expected) in [
|
||||
("arcadia 6; project mission \"x\" {}", "Unsupported schema"),
|
||||
("arcadia 7; project mission \"x\" {}", "Unsupported schema"),
|
||||
(
|
||||
"arcadia 2; project mission \"x\" { oa_activity a \"A\" { nonsense 1; } }",
|
||||
"Unknown field",
|
||||
@@ -795,7 +819,7 @@ fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensi
|
||||
assert_eq!(from_source, from_json);
|
||||
let original: serde_json::Value = serde_json::from_slice(old_json).unwrap();
|
||||
let mut retained = serde_json::to_value(&from_json).unwrap();
|
||||
assert_eq!(retained["schema_version"], 5);
|
||||
assert_eq!(retained["schema_version"], oa_core::SCHEMA_VERSION);
|
||||
retained["schema_version"] = 2.into();
|
||||
let allocations: Vec<_> = from_json
|
||||
.relations
|
||||
@@ -904,6 +928,11 @@ fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensi
|
||||
}
|
||||
}
|
||||
for diagram in retained["diagrams"].as_object_mut().unwrap().values_mut() {
|
||||
assert_eq!(
|
||||
diagram["view_options"],
|
||||
serde_json::to_value(oa_core::view_options::ViewOptions::default()).unwrap()
|
||||
);
|
||||
diagram.as_object_mut().unwrap().remove("view_options");
|
||||
assert_eq!(diagram["routes"], serde_json::json!([]));
|
||||
diagram.as_object_mut().unwrap().remove("routes");
|
||||
}
|
||||
@@ -991,7 +1020,7 @@ fn v1_text_and_json_migrate_deterministically_without_losing_legacy_semantics()
|
||||
assert_eq!(from_text, from_json);
|
||||
assert_eq!(read(text), from_text);
|
||||
assert_eq!(persistence::decode(json).unwrap(), from_json);
|
||||
assert_eq!(from_text.schema_version, 5);
|
||||
assert_eq!(from_text.schema_version, oa_core::SCHEMA_VERSION);
|
||||
assert!(!from_text.elements.values().any(|e| e.layer() == Layer::Sa));
|
||||
let raw: serde_json::Value = serde_json::from_slice(json).unwrap();
|
||||
for (id, record) in raw["elements"].as_object().unwrap() {
|
||||
@@ -1193,7 +1222,7 @@ fn complete_normative_examples_are_accepted_by_the_real_parser() {
|
||||
if code.trim_start().starts_with("arcadia 2;")
|
||||
|| code.trim_start().starts_with("arcadia 3;")
|
||||
|| code.trim_start().starts_with("arcadia 4;")
|
||||
|| code.trim_start().starts_with("arcadia 5;")
|
||||
|| code.trim_start().starts_with("arcadia 6;")
|
||||
{
|
||||
read(&code);
|
||||
examples += 1;
|
||||
|
||||
@@ -39,7 +39,10 @@ fn frozen_original_v2_reader_preserves_every_original_record() {
|
||||
assert_eq!(decoded.project, from_source.project);
|
||||
assert_eq!(decoded.migration, from_source.migration);
|
||||
let report = decoded.migration.unwrap();
|
||||
assert_eq!((report.from_schema, report.to_schema), (2, 5));
|
||||
assert_eq!(
|
||||
(report.from_schema, report.to_schema),
|
||||
(2, oa_core::SCHEMA_VERSION)
|
||||
);
|
||||
assert_eq!(report.preserved_ports.len(), 4);
|
||||
assert!(report.generated_endpoints.is_empty());
|
||||
assert_eq!(report.orientation_defaults.len(), 1);
|
||||
@@ -60,6 +63,11 @@ fn frozen_original_v2_reader_preserves_every_original_record() {
|
||||
);
|
||||
let mut current = serde_json::to_value(&decoded.project).unwrap();
|
||||
for diagram in current["diagrams"].as_object_mut().unwrap().values_mut() {
|
||||
assert_eq!(
|
||||
diagram["view_options"],
|
||||
serde_json::to_value(oa_core::view_options::ViewOptions::default()).unwrap()
|
||||
);
|
||||
diagram.as_object_mut().unwrap().remove("view_options");
|
||||
assert_eq!(diagram["routes"], json!([]), "V2 adds no authored routing");
|
||||
diagram.as_object_mut().unwrap().remove("routes");
|
||||
}
|
||||
@@ -144,7 +152,7 @@ fn missing_v2_endpoints_are_deterministic_reported_and_independently_typed() {
|
||||
}
|
||||
}
|
||||
let canonical = language::format(&first.project);
|
||||
assert!(canonical.starts_with("arcadia 5;"));
|
||||
assert!(canonical.starts_with("arcadia 6;"));
|
||||
let reopened = persistence::decode_with_report(canonical.as_bytes()).unwrap();
|
||||
assert!(reopened.migration.is_none());
|
||||
assert_eq!(reopened.project, first.project);
|
||||
@@ -274,7 +282,7 @@ fn old_contract_is_validated_before_new_shapes_and_v1_flows_through_v2() {
|
||||
assert!(error.contains("V2") && error.contains(&fe), "{error}");
|
||||
let v1 = include_bytes!("../../../examples/legacy-v1-coastal-rescue.oa.json");
|
||||
let result = persistence::decode_with_report(v1).unwrap();
|
||||
assert_eq!(result.project.schema_version, 5);
|
||||
assert_eq!(result.project.schema_version, oa_core::SCHEMA_VERSION);
|
||||
assert_eq!(result.migration.unwrap().from_schema, 1);
|
||||
assert_eq!(persistence::decode(v1).unwrap(), result.project);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,10 @@ project typed_ports "Typed ports" {
|
||||
fn source4_covers_typed_contracts_roles_allocations_and_nested_legacy_references() {
|
||||
let decoded = language::parse_with_report(SOURCE).unwrap();
|
||||
let migration = decoded.migration.as_ref().unwrap();
|
||||
assert_eq!((migration.from_schema, migration.to_schema), (4, 5));
|
||||
assert_eq!(
|
||||
(migration.from_schema, migration.to_schema),
|
||||
(4, oa_core::SCHEMA_VERSION)
|
||||
);
|
||||
let canonical = language::format(&decoded.project);
|
||||
for field in [
|
||||
"incoming_items [@data]",
|
||||
|
||||
@@ -351,7 +351,10 @@ fn migrated_equal_constructor_can_complete_semantic_review_without_model_changes
|
||||
for reverse in [false, true] {
|
||||
let decoded = persistence::decode_with_report(json.as_bytes()).unwrap();
|
||||
let report = decoded.migration.unwrap();
|
||||
assert_eq!((report.from_schema, report.to_schema), (4, 5));
|
||||
assert_eq!(
|
||||
(report.from_schema, report.to_schema),
|
||||
(4, oa_core::SCHEMA_VERSION)
|
||||
);
|
||||
let p = decoded.project;
|
||||
let t = p.traces.values().next().unwrap();
|
||||
assert!(t.baseline.as_ref().unwrap().semantic.is_none());
|
||||
|
||||
@@ -11,9 +11,12 @@ fn frozen_v4_json_and_source_preserve_the_old_review_without_fabricating_semanti
|
||||
let source = language::parse_with_report(SOURCE4).unwrap();
|
||||
assert_eq!(json.project, source.project);
|
||||
for decoded in [json, source] {
|
||||
assert_eq!(decoded.project.schema_version, 5);
|
||||
assert_eq!(decoded.project.schema_version, oa_core::SCHEMA_VERSION);
|
||||
let report = decoded.migration.unwrap();
|
||||
assert_eq!((report.from_schema, report.to_schema), (4, 5));
|
||||
assert_eq!(
|
||||
(report.from_schema, report.to_schema),
|
||||
(4, oa_core::SCHEMA_VERSION)
|
||||
);
|
||||
let trace = decoded.project.traces.values().next().unwrap();
|
||||
let baseline = trace.baseline.as_ref().unwrap();
|
||||
assert!(baseline.semantic.is_none());
|
||||
@@ -22,7 +25,7 @@ fn frozen_v4_json_and_source_preserve_the_old_review_without_fabricating_semanti
|
||||
assert_eq!(baseline.sa.name, "Approved SA name");
|
||||
assert_ne!(baseline.oa.name, decoded.project.named(trace.oa));
|
||||
let mut expected: Value = serde_json::from_slice(JSON4).unwrap();
|
||||
expected["schema_version"] = 5.into();
|
||||
expected["schema_version"] = oa_core::SCHEMA_VERSION.into();
|
||||
assert_eq!(serde_json::to_value(&decoded.project).unwrap(), expected);
|
||||
let reopened =
|
||||
persistence::decode_with_report(&serde_json::to_vec(&decoded.project).unwrap())
|
||||
@@ -117,7 +120,7 @@ fn authored_v4_modules_upgrade_only_the_version_and_preserve_nested_comments() {
|
||||
assert_eq!(compiled.bundle.files["contracts.arcadia"], module);
|
||||
assert_eq!(
|
||||
compiled.bundle.files["project.arcadia"],
|
||||
entry.replacen("arcadia 4;", "arcadia 5;", 1)
|
||||
entry.replacen("arcadia 4;", "arcadia 6;", 1)
|
||||
);
|
||||
assert_eq!(
|
||||
compiled.index.lexical_owners
|
||||
@@ -152,8 +155,11 @@ fn original_v1_v2_v3_inputs_complete_the_explicit_upgrade_to_five() {
|
||||
] {
|
||||
let decoded = persistence::decode_with_report(bytes).unwrap();
|
||||
let report = decoded.migration.unwrap();
|
||||
assert_eq!((report.from_schema, report.to_schema), (version, 5));
|
||||
assert_eq!(decoded.project.schema_version, 5);
|
||||
assert_eq!(
|
||||
(report.from_schema, report.to_schema),
|
||||
(version, oa_core::SCHEMA_VERSION)
|
||||
);
|
||||
assert_eq!(decoded.project.schema_version, oa_core::SCHEMA_VERSION);
|
||||
assert!(
|
||||
decoded
|
||||
.project
|
||||
|
||||
@@ -131,7 +131,7 @@ fn typed_historical_facets_roundtrip_deleted_references_and_all_leaf_categories(
|
||||
]);
|
||||
assert!(!model.contains(id(90)));
|
||||
let source = language::format(&model);
|
||||
assert!(source.starts_with("arcadia 5;"));
|
||||
assert!(source.starts_with("arcadia 6;"));
|
||||
assert!(
|
||||
source.contains(&format!("Named uuid \"{}\"", id(90))),
|
||||
"{source}"
|
||||
|
||||
@@ -21,7 +21,10 @@ fn genuine_v3_preserves_typed_ports_and_allocates_deterministic_relationships()
|
||||
assert_eq!(decoded.migration, source.migration);
|
||||
let project = &decoded.project;
|
||||
let report = decoded.migration.as_ref().unwrap();
|
||||
assert_eq!((report.from_schema, report.to_schema), (3, 5));
|
||||
assert_eq!(
|
||||
(report.from_schema, report.to_schema),
|
||||
(3, oa_core::SCHEMA_VERSION)
|
||||
);
|
||||
assert_eq!(project.identities().len(), 19);
|
||||
assert!(report.preserved_ports.is_empty());
|
||||
assert!(report.generated_endpoints.is_empty());
|
||||
@@ -71,7 +74,7 @@ fn genuine_v3_preserves_typed_ports_and_allocates_deterministic_relationships()
|
||||
.any(|issue| issue.code == "message_value_ambiguous_path")
|
||||
);
|
||||
let canonical = language::format(project);
|
||||
assert!(canonical.starts_with("arcadia 5;"));
|
||||
assert!(canonical.starts_with("arcadia 6;"));
|
||||
let reopened = language::parse_with_report(&canonical).unwrap();
|
||||
assert_eq!(&reopened.project, project);
|
||||
assert!(reopened.migration.is_none());
|
||||
@@ -154,12 +157,12 @@ fn v1_and_frozen_v2_inputs_reach_four_through_the_shared_data_upgrade() {
|
||||
language::parse_with_report(include_str!("fixtures/v2-coastal-rescue.arcadia")).unwrap();
|
||||
assert_eq!(v2.project, text.project);
|
||||
assert_eq!(v2.migration, text.migration);
|
||||
assert_eq!(v2.migration.unwrap().to_schema, 5);
|
||||
assert_eq!(v2.migration.unwrap().to_schema, oa_core::SCHEMA_VERSION);
|
||||
let v1 = language::parse_with_report(
|
||||
"arcadia 1; project \"Old\" id \"00000000-0000-0000-0000-000000000001\" {}",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(v1.project.schema_version, 5);
|
||||
assert_eq!(v1.project.schema_version, oa_core::SCHEMA_VERSION);
|
||||
assert_eq!(v1.migration.unwrap().from_schema, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ fn authored_v3_bundle_upgrade_preserves_includes_nested_ownership_comments_and_i
|
||||
};
|
||||
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.bundle.files.len(), 2);
|
||||
assert!(compiled.bundle.files["project.arcadia"].contains("arcadia 5;"));
|
||||
assert!(compiled.bundle.files["project.arcadia"].contains("arcadia 6;"));
|
||||
assert!(compiled.bundle.files["project.arcadia"].contains("include \"contracts.arcadia\";"));
|
||||
for text in [
|
||||
"// Contracts stay in their original module.",
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
//! Engineering oracles for the distributable current-profile lesson.
|
||||
#[path = "../../../examples/release1/model.rs"]
|
||||
mod teaching;
|
||||
use oa_core::{projection, source_project, transitions::*, validation, *};
|
||||
use teaching::id;
|
||||
|
||||
#[test]
|
||||
fn release1_teaching_regeneration_and_shipped_source_are_exact_with_one_intentional_warning() {
|
||||
let first = teaching::build();
|
||||
let second = teaching::build();
|
||||
assert_eq!(
|
||||
first.project, second.project,
|
||||
"fresh planner UUIDs must not churn the teaching source"
|
||||
);
|
||||
assert_eq!(first.before_transition, second.before_transition);
|
||||
assert_eq!(first.request, second.request);
|
||||
let bundle = teaching::checked_bundle(&first.project);
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/release1");
|
||||
for (name, bytes) in bundle.files {
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(root.join(&name)).unwrap(),
|
||||
bytes,
|
||||
"regenerate {name}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
persistence::decode(&std::fs::read(root.join("model.json")).unwrap()).unwrap(),
|
||||
first.project
|
||||
);
|
||||
for (path, expected) in [
|
||||
(
|
||||
"transition-before.json",
|
||||
serde_json::to_value(&first.before_transition).unwrap(),
|
||||
),
|
||||
(
|
||||
"transition-request.json",
|
||||
serde_json::to_value(&first.request).unwrap(),
|
||||
),
|
||||
(
|
||||
"expected-diagnostics.json",
|
||||
serde_json::to_value(validation::validate(&first.project)).unwrap(),
|
||||
),
|
||||
] {
|
||||
let shipped: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(root.join(path)).unwrap()).unwrap();
|
||||
assert_eq!(shipped, expected, "regenerate {path}");
|
||||
}
|
||||
let issues = validation::validate(&first.project);
|
||||
assert_eq!(issues.len(), 1, "{issues:?}");
|
||||
assert_eq!(issues[0].severity, validation::Severity::Warning);
|
||||
assert_eq!(issues[0].code, "work_without_behavior_context");
|
||||
assert_eq!(issues[0].element, Some(id("sa_audit_thresholds")));
|
||||
assert!(
|
||||
first
|
||||
.project
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| e.layer() == Layer::Oa)
|
||||
.all(|e| e.kind() != Kind::Port)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release1_teaching_preserves_explicit_transport_direction_reuse_and_view_identity() {
|
||||
let lesson = teaching::build();
|
||||
let p = &lesson.project;
|
||||
for prefix in ["oa", "sa"] {
|
||||
assert!(
|
||||
matches!(p.elements[&id(&format!("{prefix}_assess_step"))].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::SequenceReference { sequence: Some(target) }, .. }) if *target == id(&format!("{prefix}_assessment_procedure")))
|
||||
);
|
||||
assert!(
|
||||
matches!(p.elements[&id(&format!("{prefix}_assessment_action"))].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Action { action: Some(target), .. }, .. }) if *target == id(&format!("{prefix}_assess")))
|
||||
);
|
||||
assert!(
|
||||
p.relations
|
||||
.values()
|
||||
.any(|relation| relation.kind() == RelationKind::ChainInvolvement
|
||||
&& relation.source() == id(&format!("{prefix}_assessment_procedure"))
|
||||
&& relation.target() == id(&format!("{prefix}_triage")))
|
||||
);
|
||||
assert!(
|
||||
matches!(p.elements[&id(&format!("{prefix}_conversation"))].shared(), Some(SharedBody::Scenario { context: Some(context), .. }) if *context == id(&format!("{prefix}_restore")))
|
||||
);
|
||||
assert!(p.relations.values().any(|relation| relation.kind()
|
||||
== RelationKind::CapabilityInvolvement
|
||||
&& relation.source() == id(&format!("{prefix}_assess"))
|
||||
&& relation.target() == id(&format!("{prefix}_triage"))));
|
||||
let call = &p.elements[&id(&format!("{prefix}_call"))];
|
||||
let reply = &p.elements[&id(&format!("{prefix}_reply"))];
|
||||
let body = |e: &Element| {
|
||||
if let Some(SharedBody::Occurrence { body, .. }) = e.shared() {
|
||||
body.clone()
|
||||
} else {
|
||||
panic!()
|
||||
}
|
||||
};
|
||||
let validation::MessageEndpointResolution::Resolved { source, target } =
|
||||
validation::resolve_message_endpoints(p, &body(call))
|
||||
else {
|
||||
panic!("call not resolved")
|
||||
};
|
||||
assert_eq!(
|
||||
validation::resolve_message_endpoints(p, &body(reply)),
|
||||
validation::MessageEndpointResolution::Resolved {
|
||||
source: target,
|
||||
target: source
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
matches!(p.elements[&id(&format!("{prefix}_use_field"))].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::SequenceReference { sequence: Some(target) }, .. }) if *target == id(&format!("{prefix}_field_procedure")))
|
||||
);
|
||||
assert!(
|
||||
matches!(p.elements[&id(&format!("{prefix}_use_conversation"))].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::ScenarioUse { scenario: Some(target), bindings }, .. }) if *target == id(&format!("{prefix}_conversation")) && bindings.len() == 2)
|
||||
);
|
||||
}
|
||||
let full = projection::project_view(p, id("sa_allocation_view"));
|
||||
let focused = projection::project_view(p, id("sa_service_view"));
|
||||
let edges: std::collections::BTreeSet<_> = full.edges.iter().map(|e| e.id).collect();
|
||||
assert_eq!(
|
||||
edges,
|
||||
[
|
||||
id("sa_contact"),
|
||||
id("sa_repair_request"),
|
||||
id("sa_repair_report")
|
||||
]
|
||||
.into()
|
||||
);
|
||||
assert_eq!(focused.edges.len(), 1);
|
||||
assert_eq!(focused.edges[0].id, id("sa_contact"));
|
||||
assert!(
|
||||
p.diagrams[&id("sa_service_view")]
|
||||
.view_options
|
||||
.exchange_labels
|
||||
.label(p, focused.edges[0].id)
|
||||
.contains("Valve repair")
|
||||
);
|
||||
let ports = |view: &projection::ProjectedView| {
|
||||
view.ports
|
||||
.iter()
|
||||
.map(|v| (v.element, v.position))
|
||||
.collect::<std::collections::BTreeMap<_, _>>()
|
||||
};
|
||||
assert_eq!(
|
||||
ports(&full),
|
||||
ports(&focused),
|
||||
"filtering must not move the shared technical port geometry"
|
||||
);
|
||||
assert!(
|
||||
p.traces
|
||||
.values()
|
||||
.any(|t| t.oa == id("oa_contact") && t.sa == id("sa_contact"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release1_teaching_rejects_guessed_ports_and_wrong_performer_without_changing_source() {
|
||||
let lesson = teaching::build();
|
||||
let mut missing_decisions = lesson.request.clone();
|
||||
missing_decisions.communication_endpoints.clear();
|
||||
let plan = plan_transition(&lesson.before_transition, &missing_decisions).unwrap();
|
||||
assert!(
|
||||
!plan.conflicts.is_empty(),
|
||||
"an operational channel does not determine technical ports"
|
||||
);
|
||||
let compiled = source_project::compile_bundle(
|
||||
&teaching::checked_bundle(&lesson.project),
|
||||
source_project::ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut wrong = lesson.project.relations[&id("oa_repair_request")].clone();
|
||||
wrong.body.set_endpoints(id("oa_assess"), id("oa_reset"));
|
||||
assert!(
|
||||
execute_batch(
|
||||
&lesson.project,
|
||||
&[Command::EditRelation(wrong.id, wrong.clone())]
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let mut candidate = lesson.project.clone();
|
||||
candidate.relations.insert(wrong.id, wrong);
|
||||
assert!(
|
||||
validation::validate(&candidate)
|
||||
.iter()
|
||||
.any(|i| i.severity == validation::Severity::Error
|
||||
&& i.code == "exchange_allocation_performer")
|
||||
);
|
||||
assert!(
|
||||
source_project::update_bundle(
|
||||
&compiled,
|
||||
&candidate,
|
||||
source_project::ProjectLimits::default()
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(compiled.decoded.project, lesson.project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release1_teaching_review_changes_only_the_selected_counterpart_and_preserves_modules() {
|
||||
let lesson = teaching::build();
|
||||
let mut changed = lesson.project.clone();
|
||||
changed.elements.get_mut(&id("oa_confirm")).unwrap().name = "Confirm stable pressure".into();
|
||||
let compiled = source_project::compile_bundle(
|
||||
&teaching::checked_bundle(&changed),
|
||||
source_project::ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut transition =
|
||||
TransitionRequest::new(TransitionDirection::OaToSa, vec![id("oa_confirm")]);
|
||||
transition.fields.clear();
|
||||
transition.mappings.push(TransitionMapping {
|
||||
source: id("oa_confirm"),
|
||||
target: Some(id("sa_confirm")),
|
||||
target_kind: None,
|
||||
});
|
||||
let mut request = SemanticTransitionRequest::new(transition);
|
||||
request.reviews.push(SemanticReviewRequest {
|
||||
source: id("oa_confirm"),
|
||||
resolutions: vec![FacetResolution {
|
||||
facet: FacetSelector::Header(HeaderFacet::Name),
|
||||
decision: SemanticDecision::TakeSource,
|
||||
}],
|
||||
reset: vec![],
|
||||
});
|
||||
let plan = plan_semantic_transition(&changed, request).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
let accepted = apply_semantic_transition(&changed, &plan).unwrap();
|
||||
assert_eq!(
|
||||
accepted.elements[&id("sa_confirm")].name,
|
||||
"Confirm stable pressure"
|
||||
);
|
||||
assert_eq!(
|
||||
accepted.elements[&id("sa_control")].name,
|
||||
lesson.project.elements[&id("sa_control")].name
|
||||
);
|
||||
let updated = source_project::update_bundle(
|
||||
&compiled,
|
||||
&accepted,
|
||||
source_project::ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(updated.decoded.project, accepted);
|
||||
assert_eq!(
|
||||
updated.bundle.files.keys().collect::<Vec<_>>(),
|
||||
compiled.bundle.files.keys().collect::<Vec<_>>()
|
||||
);
|
||||
for (path, original) in &compiled.bundle.files {
|
||||
if path != "sa/functions/function_sa_restore_work.arcadia" && path != "traceability.arcadia"
|
||||
{
|
||||
assert_eq!(
|
||||
&updated.bundle.files[path], original,
|
||||
"unrelated module changed: {path}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -733,12 +733,36 @@ fn transitions_create_sa_ports_and_omit_sa_implementation_from_portless_oa() {
|
||||
0
|
||||
);
|
||||
let (mut p, a, b, _, fe) = fixture();
|
||||
allocate(&mut p, a, b);
|
||||
let (system, partner) = allocate(&mut p, a, b);
|
||||
let component_req = component_request(&p, fe, ComponentPortKind::Flow);
|
||||
let boundary = boundary_id(&component_req);
|
||||
p = execute_batch(&p, &plan_component_exchange(&p, &component_req).unwrap()).unwrap();
|
||||
let mut reverse = TransitionRequest::new(TransitionDirection::SaToOa, vec![fe, boundary]);
|
||||
reverse.scope = DependencyScope::Required;
|
||||
// A boundary now has a portless OA CommunicationMean counterpart. Its
|
||||
// participant owners require an explicit operational boundary decision.
|
||||
let incomplete = plan_transition(&p, &reverse).unwrap();
|
||||
for owner in [system, partner] {
|
||||
assert!(
|
||||
incomplete
|
||||
.conflicts
|
||||
.iter()
|
||||
.any(|conflict| conflict.source == Some(owner)
|
||||
&& conflict.message.contains("counterpart kind explicitly"))
|
||||
);
|
||||
}
|
||||
reverse.mappings = vec![
|
||||
TransitionMapping {
|
||||
source: system,
|
||||
target: None,
|
||||
target_kind: Some(Kind::Entity),
|
||||
},
|
||||
TransitionMapping {
|
||||
source: partner,
|
||||
target: None,
|
||||
target_kind: Some(Kind::Actor),
|
||||
},
|
||||
];
|
||||
let plan = plan_transition(&p, &reverse).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
assert!(plan.warnings.iter().any(|w| w.contains("SA-only omission")));
|
||||
@@ -757,6 +781,28 @@ fn transitions_create_sa_ports_and_omit_sa_implementation_from_portless_oa() {
|
||||
.values()
|
||||
.any(|r| r.kind() == RelationKind::Interaction)
|
||||
);
|
||||
let mean = result
|
||||
.traces
|
||||
.values()
|
||||
.find(|trace| trace.sa == boundary)
|
||||
.unwrap()
|
||||
.oa;
|
||||
let relation = &result.relations[&mean];
|
||||
assert_eq!(relation.kind(), RelationKind::CommunicationMean);
|
||||
assert_eq!(result.elements[&relation.source()].kind(), Kind::Entity);
|
||||
assert_eq!(result.elements[&relation.target()].kind(), Kind::Actor);
|
||||
assert!(
|
||||
result
|
||||
.traces
|
||||
.values()
|
||||
.any(|trace| trace.sa == system && trace.oa == relation.source())
|
||||
);
|
||||
assert!(
|
||||
result
|
||||
.traces
|
||||
.values()
|
||||
.any(|trace| trace.sa == partner && trace.oa == relation.target())
|
||||
);
|
||||
for e in p.elements.values() {
|
||||
assert_eq!(result.elements[&e.id], *e);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ fn fixture() -> Fixture {
|
||||
description: String::new(),
|
||||
layer: Layer::Sa,
|
||||
projection: ProjectionKind::Overview,
|
||||
view_options: Default::default(),
|
||||
nodes: BTreeMap::new(),
|
||||
routes: vec![],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
//! Independent GUI boundary: Presence observes attachment, not record existence.
|
||||
use oa_core::{transitions::*, *};
|
||||
|
||||
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: "urn:engineering:requirement".into(),
|
||||
revision: None,
|
||||
fragment: Some(String::new()),
|
||||
},
|
||||
);
|
||||
id
|
||||
}
|
||||
|
||||
fn fixture(reverse: bool) -> (Project, Id, Id, TransitionDirection) {
|
||||
let mut p = Project::empty();
|
||||
let oa = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "OA owner", None)
|
||||
.unwrap();
|
||||
let sa = p
|
||||
.add_element(Kind::Function, Layer::Sa, "SA owner", None)
|
||||
.unwrap();
|
||||
let id = Id::new_v4();
|
||||
p.traces.insert(
|
||||
id,
|
||||
Trace {
|
||||
id,
|
||||
alias: format!("trace_{}", id.simple()),
|
||||
name: "Owner 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,
|
||||
}),
|
||||
},
|
||||
);
|
||||
if reverse {
|
||||
(p, sa, oa, TransitionDirection::SaToOa)
|
||||
} else {
|
||||
(p, oa, sa, TransitionDirection::OaToSa)
|
||||
}
|
||||
}
|
||||
|
||||
fn removal(
|
||||
source: Id,
|
||||
target: Id,
|
||||
direction: TransitionDirection,
|
||||
source_link: Option<Id>,
|
||||
target_link: Id,
|
||||
) -> SemanticTransitionRequest {
|
||||
let mut transition = TransitionRequest::new(direction, vec![source]);
|
||||
transition.mappings.push(TransitionMapping {
|
||||
source,
|
||||
target: Some(target),
|
||||
target_kind: None,
|
||||
});
|
||||
let mut request = SemanticTransitionRequest::new(transition);
|
||||
let keys = if direction == TransitionDirection::OaToSa {
|
||||
KeyPair {
|
||||
oa: source_link,
|
||||
sa: Some(target_link),
|
||||
}
|
||||
} else {
|
||||
KeyPair {
|
||||
oa: Some(target_link),
|
||||
sa: source_link,
|
||||
}
|
||||
};
|
||||
if let (Some(oa), Some(sa)) = (keys.oa, keys.sa) {
|
||||
request.references.insert(oa, sa);
|
||||
}
|
||||
request.reviews.push(SemanticReviewRequest {
|
||||
source,
|
||||
reset: vec![],
|
||||
resolutions: vec![FacetResolution {
|
||||
facet: FacetSelector::Artifact {
|
||||
keys,
|
||||
facet: ArtifactFacet::Presence,
|
||||
},
|
||||
decision: SemanticDecision::TakeSource,
|
||||
}],
|
||||
});
|
||||
request
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_only_artifact_take_preserves_without_fabricated_source_identity() {
|
||||
for reverse in [false, true] {
|
||||
let (mut p, source, target, direction) = fixture(reverse);
|
||||
let link = artifact(&mut p, target, "Target-only requirement");
|
||||
let before = p.clone();
|
||||
let plan =
|
||||
plan_semantic_transition(&p, removal(source, target, direction, None, link)).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
assert_eq!(p, before, "planning must be pure");
|
||||
let after = apply_semantic_transition(&p, &plan).unwrap();
|
||||
assert_eq!(after.artifact_links, before.artifact_links);
|
||||
assert_eq!(after.elements, before.elements);
|
||||
assert_eq!(after.relations, before.relations);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moved_away_source_artifact_counts_as_absent_from_reviewed_owner() {
|
||||
for reverse in [false, true] {
|
||||
let (mut p, source, target, direction) = fixture(reverse);
|
||||
let (kind, layer) = if reverse {
|
||||
(Kind::Function, Layer::Sa)
|
||||
} else {
|
||||
(Kind::Activity, Layer::Oa)
|
||||
};
|
||||
let elsewhere = p
|
||||
.add_element(kind, layer, "Moved source owner", None)
|
||||
.unwrap();
|
||||
let source_link = artifact(&mut p, elsewhere, "Still exists elsewhere");
|
||||
let target_link = artifact(&mut p, target, "Old counterpart attachment");
|
||||
let original_source = p.artifact_links[&source_link].clone();
|
||||
let plan = plan_semantic_transition(
|
||||
&p,
|
||||
removal(source, target, direction, Some(source_link), target_link),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
let after = apply_semantic_transition(&p, &plan).unwrap();
|
||||
assert!(!after.artifact_links.contains_key(&target_link));
|
||||
assert_eq!(after.artifact_links[&source_link], original_source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
//! Direct core oracles for the next existing-record graphical attachment slice.
|
||||
use oa_core::{transitions::*, *};
|
||||
|
||||
struct Fixture {
|
||||
p: Project,
|
||||
owners: [Id; 2],
|
||||
attached: [Id; 2],
|
||||
outside: [Id; 2],
|
||||
trace: Id,
|
||||
child: bool,
|
||||
end: IncidentEnd,
|
||||
}
|
||||
fn trace(p: &mut Project, oa: Id, sa: Id, name: &str) -> Id {
|
||||
let id = Id::new_v4();
|
||||
p.traces.insert(
|
||||
id,
|
||||
Trace {
|
||||
id,
|
||||
alias: format!("trace_{}", id.simple()),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
oa,
|
||||
sa,
|
||||
kind: TraceKind::Manual,
|
||||
baseline: None,
|
||||
},
|
||||
);
|
||||
id
|
||||
}
|
||||
fn fixture(child: bool, end: IncidentEnd) -> Fixture {
|
||||
let mut p = Project::empty();
|
||||
let mut owners = vec![];
|
||||
let mut attached = vec![];
|
||||
let mut outside = vec![];
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let kind = if child {
|
||||
if layer == Layer::Oa {
|
||||
Kind::Activity
|
||||
} else {
|
||||
Kind::Function
|
||||
}
|
||||
} else {
|
||||
Kind::DataType
|
||||
};
|
||||
let owner = p.add_element(kind, layer, "Current owner", None).unwrap();
|
||||
let replacement = p
|
||||
.add_element(kind, layer, "Reviewed replacement", None)
|
||||
.unwrap();
|
||||
let record = if child {
|
||||
p.add_element(kind, layer, "Attached work", Some(owner))
|
||||
.unwrap()
|
||||
} else {
|
||||
let peer = p.add_element(kind, layer, "Other endpoint", None).unwrap();
|
||||
let (source, target) = if end == IncidentEnd::Source {
|
||||
(owner, peer)
|
||||
} else {
|
||||
(peer, owner)
|
||||
};
|
||||
p.relate(
|
||||
RelationKind::DataAssociation,
|
||||
"Incident data association",
|
||||
source,
|
||||
target,
|
||||
)
|
||||
};
|
||||
owners.push(owner);
|
||||
attached.push(record);
|
||||
outside.push(replacement);
|
||||
}
|
||||
for element in p.elements.values_mut() {
|
||||
if let Some(SharedBody::DataType { definition, .. }) = element.shared_mut() {
|
||||
*definition = DataTypeDefinition::Record { supertype: None };
|
||||
}
|
||||
}
|
||||
let trace = trace(&mut p, owners[0], owners[1], "Owner attachment rationale");
|
||||
validate_candidate(&p).unwrap();
|
||||
Fixture {
|
||||
p,
|
||||
owners: [owners[0], owners[1]],
|
||||
attached: [attached[0], attached[1]],
|
||||
outside: [outside[0], outside[1]],
|
||||
trace,
|
||||
child,
|
||||
end,
|
||||
}
|
||||
}
|
||||
fn selector(f: &Fixture) -> FacetSelector {
|
||||
let keys = KeyPair {
|
||||
oa: Some(f.attached[0]),
|
||||
sa: Some(f.attached[1]),
|
||||
};
|
||||
if f.child {
|
||||
FacetSelector::Child(keys)
|
||||
} else {
|
||||
FacetSelector::Incident { keys, end: f.end }
|
||||
}
|
||||
}
|
||||
fn candidate(f: &Fixture, side: usize) -> SemanticRecord {
|
||||
if f.child {
|
||||
let mut value = f.p.elements[&f.attached[side]].clone();
|
||||
value.parent = Some(f.outside[side]);
|
||||
SemanticRecord::Element(value)
|
||||
} else {
|
||||
let mut value = f.p.relations[&f.attached[side]].clone();
|
||||
let (a, b) = value.body.endpoints();
|
||||
value.body.set_endpoints(
|
||||
if f.end == IncidentEnd::Source {
|
||||
f.outside[side]
|
||||
} else {
|
||||
a
|
||||
},
|
||||
if f.end == IncidentEnd::Target {
|
||||
f.outside[side]
|
||||
} else {
|
||||
b
|
||||
},
|
||||
);
|
||||
SemanticRecord::Relation(value)
|
||||
}
|
||||
}
|
||||
fn request(f: &Fixture, reverse: bool, decision: SemanticDecision) -> SemanticTransitionRequest {
|
||||
let source = usize::from(reverse);
|
||||
let target = 1 - source;
|
||||
let mut transition = TransitionRequest::new(
|
||||
if reverse {
|
||||
TransitionDirection::SaToOa
|
||||
} else {
|
||||
TransitionDirection::OaToSa
|
||||
},
|
||||
vec![f.owners[source]],
|
||||
);
|
||||
transition.mappings.push(TransitionMapping {
|
||||
source: f.owners[source],
|
||||
target: Some(f.owners[target]),
|
||||
target_kind: None,
|
||||
});
|
||||
let mut result = SemanticTransitionRequest::new(transition);
|
||||
result.traces.insert(f.owners[source], f.trace);
|
||||
result.references.insert(f.owners[0], f.owners[1]);
|
||||
result.references.insert(f.attached[0], f.attached[1]);
|
||||
result.reviews.push(SemanticReviewRequest {
|
||||
source: f.owners[source],
|
||||
resolutions: vec![FacetResolution {
|
||||
facet: selector(f),
|
||||
decision,
|
||||
}],
|
||||
reset: vec![],
|
||||
});
|
||||
result
|
||||
}
|
||||
fn prepared(p: &Project, request: SemanticTransitionRequest) -> (Project, SemanticTransitionPlan) {
|
||||
let plan = plan_semantic_transition(p, request).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
let result = prepare_semantic_transition(p, &plan).unwrap().project;
|
||||
(result, plan)
|
||||
}
|
||||
#[test]
|
||||
fn attachment_candidate_changes_existing_parent_or_endpoint_without_attached_record_trace() {
|
||||
for child in [true, false] {
|
||||
for end in [IncidentEnd::Source, IncidentEnd::Target] {
|
||||
if child && end == IncidentEnd::Target {
|
||||
continue;
|
||||
}
|
||||
for reverse in [false, true] {
|
||||
let f = fixture(child, end);
|
||||
assert_eq!(f.p.traces.len(), 1);
|
||||
let side = usize::from(!reverse);
|
||||
let expected = candidate(&f, side);
|
||||
let mut request = request(&f, reverse, SemanticDecision::Candidate);
|
||||
request.candidates.push(expected.clone());
|
||||
let original_source = semantic_record(&f.p, f.attached[1 - side]);
|
||||
let (result, plan) = prepared(&f.p, request);
|
||||
assert_eq!(semantic_record(&result, f.attached[side]), Some(expected));
|
||||
assert_eq!(
|
||||
semantic_record(&result, f.attached[1 - side]),
|
||||
original_source
|
||||
);
|
||||
assert_eq!(
|
||||
result.traces.len(),
|
||||
1,
|
||||
"No relation/child trace is invented"
|
||||
);
|
||||
assert_eq!(plan.reviews[0].acknowledged, vec![selector(&f)]);
|
||||
let source = language::format(&result);
|
||||
assert_eq!(language::parse(&source).unwrap(), result);
|
||||
let mut document = f.p.clone();
|
||||
let receipt = apply_semantic_transition(&document, &plan).unwrap();
|
||||
document = receipt;
|
||||
assert_eq!(document, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn incident_absence_take_refuses_but_explicit_candidate_endpoint_is_reviewable() {
|
||||
for end in [IncidentEnd::Source, IncidentEnd::Target] {
|
||||
for reverse in [false, true] {
|
||||
let mut f = fixture(false, end);
|
||||
let source = usize::from(reverse);
|
||||
let SemanticRecord::Relation(detached) = candidate(&f, source) else {
|
||||
unreachable!()
|
||||
};
|
||||
f.p.relations.insert(f.attached[source], detached);
|
||||
let take =
|
||||
plan_semantic_transition(&f.p, request(&f, reverse, SemanticDecision::TakeSource))
|
||||
.unwrap();
|
||||
assert!(
|
||||
take.conflicts
|
||||
.iter()
|
||||
.any(|e| e.contains("explicit final endpoint")),
|
||||
"{:?}",
|
||||
take.conflicts
|
||||
);
|
||||
let mut request = request(&f, reverse, SemanticDecision::Candidate);
|
||||
request.candidates.push(candidate(&f, 1 - source));
|
||||
let _ = prepared(&f.p, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn attachment_candidate_and_direct_record_keep_conflict_by_actual_slot_in_either_order() {
|
||||
for child in [true, false] {
|
||||
for end in [IncidentEnd::Source, IncidentEnd::Target] {
|
||||
if child && end == IncidentEnd::Target {
|
||||
continue;
|
||||
}
|
||||
for reverse in [false, true] {
|
||||
let mut f = fixture(child, end);
|
||||
let linked = trace(
|
||||
&mut f.p,
|
||||
f.attached[0],
|
||||
f.attached[1],
|
||||
"Independent record rationale",
|
||||
);
|
||||
let source = usize::from(reverse);
|
||||
let target = 1 - source;
|
||||
let mut request = request(&f, reverse, SemanticDecision::Candidate);
|
||||
request.candidates.push(candidate(&f, target));
|
||||
request.transition.sources.push(f.attached[source]);
|
||||
request.transition.mappings.push(TransitionMapping {
|
||||
source: f.attached[source],
|
||||
target: Some(f.attached[target]),
|
||||
target_kind: None,
|
||||
});
|
||||
request.traces.insert(f.attached[source], linked);
|
||||
let facet = if child {
|
||||
FacetSelector::Header(HeaderFacet::Parent)
|
||||
} else {
|
||||
FacetSelector::Relation(if end == IncidentEnd::Source {
|
||||
RelationFacet::Source
|
||||
} else {
|
||||
RelationFacet::Target
|
||||
})
|
||||
};
|
||||
request.reviews.push(SemanticReviewRequest {
|
||||
source: f.attached[source],
|
||||
resolutions: vec![FacetResolution {
|
||||
facet,
|
||||
decision: SemanticDecision::KeepTarget,
|
||||
}],
|
||||
reset: vec![],
|
||||
});
|
||||
for permute in [false, true] {
|
||||
let mut request = request.clone();
|
||||
if permute {
|
||||
request.reviews.reverse();
|
||||
request.transition.sources.reverse();
|
||||
request.transition.mappings.reverse();
|
||||
}
|
||||
let plan = plan_semantic_transition(&f.p, request).unwrap();
|
||||
assert!(
|
||||
plan.conflicts.iter().any(|e| e.contains("Contradictory")),
|
||||
"{child}/{end:?}/{reverse}: {:?}",
|
||||
plan.conflicts
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convergent_owner_attachment_candidates_agree_and_keep_cannot_override_them() {
|
||||
for child in [true, false] {
|
||||
for end in [IncidentEnd::Source, IncidentEnd::Target] {
|
||||
if child && end == IncidentEnd::Target {
|
||||
continue;
|
||||
}
|
||||
let mut f = fixture(child, end);
|
||||
let second_owner =
|
||||
f.p.add_element(
|
||||
f.p.elements[&f.owners[0]].kind(),
|
||||
Layer::Oa,
|
||||
"Second operational concern",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(SharedBody::DataType { definition, .. }) =
|
||||
f.p.elements.get_mut(&second_owner).unwrap().shared_mut()
|
||||
{
|
||||
*definition = DataTypeDefinition::Record { supertype: None };
|
||||
}
|
||||
let second_record = Id::new_v4();
|
||||
if child {
|
||||
let mut value = f.p.elements[&f.attached[0]].clone();
|
||||
value.id = second_record;
|
||||
value.alias = format!("second_{}", second_record.simple());
|
||||
value.parent = Some(second_owner);
|
||||
f.p.elements.insert(second_record, value);
|
||||
} else {
|
||||
let mut value = f.p.relations[&f.attached[0]].clone();
|
||||
value.id = second_record;
|
||||
value.alias = format!("second_{}", second_record.simple());
|
||||
let (a, b) = value.body.endpoints();
|
||||
value.body.set_endpoints(
|
||||
if end == IncidentEnd::Source {
|
||||
second_owner
|
||||
} else {
|
||||
a
|
||||
},
|
||||
if end == IncidentEnd::Target {
|
||||
second_owner
|
||||
} else {
|
||||
b
|
||||
},
|
||||
);
|
||||
f.p.relations.insert(second_record, value);
|
||||
}
|
||||
let second_trace = trace(
|
||||
&mut f.p,
|
||||
second_owner,
|
||||
f.owners[1],
|
||||
"Second owner correspondence",
|
||||
);
|
||||
validate_candidate(&f.p).unwrap();
|
||||
let mut request = request(&f, false, SemanticDecision::Candidate);
|
||||
request.candidates.push(candidate(&f, 1));
|
||||
request.transition.sources.push(second_owner);
|
||||
request.transition.mappings.push(TransitionMapping {
|
||||
source: second_owner,
|
||||
target: Some(f.owners[1]),
|
||||
target_kind: None,
|
||||
});
|
||||
request.references.insert(second_owner, f.owners[1]);
|
||||
request.references.insert(second_record, f.attached[1]);
|
||||
request.traces.insert(second_owner, second_trace);
|
||||
let keys = KeyPair {
|
||||
oa: Some(second_record),
|
||||
sa: Some(f.attached[1]),
|
||||
};
|
||||
let facet = if child {
|
||||
FacetSelector::Child(keys)
|
||||
} else {
|
||||
FacetSelector::Incident { keys, end }
|
||||
};
|
||||
request.reviews.push(SemanticReviewRequest {
|
||||
source: second_owner,
|
||||
resolutions: vec![FacetResolution {
|
||||
facet,
|
||||
decision: SemanticDecision::Candidate,
|
||||
}],
|
||||
reset: vec![],
|
||||
});
|
||||
let (agreed, _) = prepared(&f.p, request.clone());
|
||||
assert_eq!(
|
||||
semantic_record(&agreed, f.attached[1]),
|
||||
Some(candidate(&f, 1))
|
||||
);
|
||||
for keep in [false, true] {
|
||||
for permute in [false, true] {
|
||||
let mut review = request.clone();
|
||||
if keep {
|
||||
review.reviews[1].resolutions[0].decision = SemanticDecision::KeepTarget;
|
||||
}
|
||||
if permute {
|
||||
review.reviews.reverse();
|
||||
review.transition.sources.reverse();
|
||||
review.transition.mappings.reverse();
|
||||
}
|
||||
let plan = plan_semantic_transition(&f.p, review).unwrap();
|
||||
if keep {
|
||||
assert!(
|
||||
plan.conflicts.iter().any(|e| e.contains("Contradictory")),
|
||||
"{:?}",
|
||||
plan.conflicts
|
||||
);
|
||||
} else {
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
assert_eq!(
|
||||
prepare_semantic_transition(&f.p, &plan).unwrap().project,
|
||||
agreed
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
use oa_core::{source_project::*, transitions::*, view_options::*, *};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn source6_traceability_default_keeps_both_layers_and_indexed_projection_matches() {
|
||||
let mut p = Project::empty();
|
||||
let oa = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "OA send", None)
|
||||
.unwrap();
|
||||
let ob = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "OA receive", None)
|
||||
.unwrap();
|
||||
let sa = p
|
||||
.add_element(Kind::Function, Layer::Sa, "SA send", None)
|
||||
.unwrap();
|
||||
let sb = p
|
||||
.add_element(Kind::Function, Layer::Sa, "SA receive", None)
|
||||
.unwrap();
|
||||
let interaction = p.relate(RelationKind::Interaction, "OA report", oa, ob);
|
||||
let fe = Id::new_v4();
|
||||
let request = FunctionalExchangeRequest {
|
||||
id: fe,
|
||||
alias: "sa_report".into(),
|
||||
name: "SA report".into(),
|
||||
description: String::new(),
|
||||
source: sa,
|
||||
target: sb,
|
||||
source_port: PortChoice::Auto,
|
||||
target_port: PortChoice::Auto,
|
||||
items: vec![],
|
||||
};
|
||||
p = execute_batch(&p, &plan_functional_exchange(&p, &request).unwrap()).unwrap();
|
||||
let d = p.ensure_diagram(Layer::Oa);
|
||||
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Traceability;
|
||||
for (i, id) in [oa, ob, sa, sb].into_iter().enumerate() {
|
||||
p.place(
|
||||
d,
|
||||
id,
|
||||
Position {
|
||||
x: 300. * i as f32,
|
||||
y: 0.,
|
||||
},
|
||||
);
|
||||
}
|
||||
let index = ViewIndex::new(&p);
|
||||
let view = projection::project_view_with_index(&p, d, &index);
|
||||
assert!(view.edges.iter().any(|e| e.id == interaction));
|
||||
assert!(view.edges.iter().any(|e| e.id == fe));
|
||||
assert_eq!(view, projection::project_view(&p, d));
|
||||
p.diagrams
|
||||
.get_mut(&d)
|
||||
.unwrap()
|
||||
.view_options
|
||||
.hidden_edges
|
||||
.push(fe);
|
||||
validate_candidate(&p).unwrap();
|
||||
assert!(
|
||||
!projection::project_view_with_index(&p, d, &index)
|
||||
.edges
|
||||
.iter()
|
||||
.any(|e| e.id == fe)
|
||||
);
|
||||
assert!(
|
||||
projection::project_view_with_index(&p, d, &index)
|
||||
.edges
|
||||
.iter()
|
||||
.any(|e| e.id == interaction)
|
||||
);
|
||||
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Overview;
|
||||
assert!(validate_candidate(&p).is_err());
|
||||
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Traceability;
|
||||
p.diagrams.get_mut(&d).unwrap().view_options.relation_kinds = RelationKinds::Only(vec![]);
|
||||
assert!(
|
||||
projection::project_view_with_index(&p, d, &index)
|
||||
.edges
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source6_edge_availability_uses_actual_shared_projection_fold_and_placement_rules() {
|
||||
let mut p = Project::empty();
|
||||
let parent = p
|
||||
.add_element(Kind::Entity, Layer::Oa, "Team", None)
|
||||
.unwrap();
|
||||
let a = p
|
||||
.add_element(Kind::Entity, Layer::Oa, "A", Some(parent))
|
||||
.unwrap();
|
||||
let b = p
|
||||
.add_element(Kind::Entity, Layer::Oa, "B", Some(parent))
|
||||
.unwrap();
|
||||
let mean = p.relate(
|
||||
RelationKind::CommunicationMean,
|
||||
"Internal communication",
|
||||
a,
|
||||
b,
|
||||
);
|
||||
let d = p.ensure_diagram(Layer::Oa);
|
||||
let outer = p.place(d, parent, Position { x: 0., y: 0. });
|
||||
p.place(d, a, Position { x: 20., y: 100. });
|
||||
let nb = p.place(d, b, Position { x: 350., y: 100. });
|
||||
assert_eq!(
|
||||
projection::edge_availability(&p, &p.diagrams[&d])[&mean],
|
||||
EdgeAvailability::Visible
|
||||
);
|
||||
p.diagrams
|
||||
.get_mut(&d)
|
||||
.unwrap()
|
||||
.view_options
|
||||
.hidden_edges
|
||||
.push(mean);
|
||||
assert_eq!(
|
||||
projection::edge_availability(&p, &p.diagrams[&d])[&mean],
|
||||
EdgeAvailability::Visible
|
||||
);
|
||||
p.diagrams
|
||||
.get_mut(&d)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&outer)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
assert_eq!(
|
||||
projection::edge_availability(&p, &p.diagrams[&d])[&mean],
|
||||
EdgeAvailability::FoldedInternal
|
||||
);
|
||||
p.diagrams
|
||||
.get_mut(&d)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&outer)
|
||||
.unwrap()
|
||||
.collapsed = false;
|
||||
p.diagrams.get_mut(&d).unwrap().nodes.remove(&nb);
|
||||
assert_eq!(
|
||||
projection::edge_availability(&p, &p.diagrams[&d])[&mean],
|
||||
EdgeAvailability::EndpointNotShown {
|
||||
source: false,
|
||||
target: true
|
||||
}
|
||||
);
|
||||
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::FunctionalDataflow;
|
||||
assert_eq!(
|
||||
projection::edge_availability(&p, &p.diagrams[&d])[&mean],
|
||||
EdgeAvailability::OutsideProjection
|
||||
);
|
||||
}
|
||||
|
||||
fn setup() -> (Project, Id, Id, Id, Id) {
|
||||
let mut p = Project::empty();
|
||||
let a = p.add_element(Kind::Entity, Layer::Oa, "A", None).unwrap();
|
||||
let b = p.add_element(Kind::Actor, Layer::Oa, "B", None).unwrap();
|
||||
let exchange = p.relate(RelationKind::CommunicationMean, "Communication", a, b);
|
||||
let category = p
|
||||
.add_element(
|
||||
Kind::ExchangeCategory,
|
||||
Layer::Oa,
|
||||
"Participant exchanges",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&category).unwrap().shared_mut().unwrap() = SharedBody::ExchangeCategory {
|
||||
family: ExchangeFamily::Participant,
|
||||
exchanges: vec![exchange],
|
||||
};
|
||||
let diagram = p.ensure_diagram(Layer::Oa);
|
||||
p.place(diagram, a, Position { x: 0., y: 0. });
|
||||
p.place(diagram, b, Position { x: 400., y: 0. });
|
||||
p.diagrams.get_mut(&diagram).unwrap().view_options = ViewOptions {
|
||||
participant_categories: CategorySelection::Any(vec![category]),
|
||||
hidden_edges: vec![exchange],
|
||||
exchange_labels: ExchangeLabels::NameAndItems,
|
||||
..Default::default()
|
||||
};
|
||||
(p, exchange, category, diagram, a)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source6_views_round_trip_strict_fields_and_reference_aliases() {
|
||||
let (p, _, category, _, _) = setup();
|
||||
validate_candidate(&p).unwrap();
|
||||
let source = language::format(&p);
|
||||
assert!(source.contains("exchange_category"));
|
||||
assert!(source.contains(&format!("@{}", p.elements[&category].alias)));
|
||||
assert_eq!(language::parse(&source).unwrap(), p);
|
||||
assert_eq!(
|
||||
persistence::decode(&serde_json::to_vec(&p).unwrap()).unwrap(),
|
||||
p
|
||||
);
|
||||
let bundle = SourceBundle {
|
||||
entry: "model.arcadia".into(),
|
||||
files: BTreeMap::from([("model.arcadia".into(), source)]),
|
||||
};
|
||||
assert_eq!(
|
||||
compile_bundle(&bundle, ProjectLimits::default())
|
||||
.unwrap()
|
||||
.decoded
|
||||
.project,
|
||||
p
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source6_view_defaults_cannot_be_smuggled_under_old_json_headers() {
|
||||
let mut p = Project::empty();
|
||||
p.ensure_diagram(Layer::Oa);
|
||||
let current = serde_json::to_value(&p).unwrap();
|
||||
for version in 3..=5 {
|
||||
let mut old = current.clone();
|
||||
old["schema_version"] = version.into();
|
||||
for diagram in old["diagrams"].as_object_mut().unwrap().values_mut() {
|
||||
diagram["view_options"] = serde_json::json!({});
|
||||
}
|
||||
let error = persistence::decode(&serde_json::to_vec(&old).unwrap())
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(
|
||||
error.contains("requires schema/source 6"),
|
||||
"{version}: {error}"
|
||||
);
|
||||
for diagram in old["diagrams"].as_object_mut().unwrap().values_mut() {
|
||||
diagram.as_object_mut().unwrap().remove("view_options");
|
||||
}
|
||||
assert_eq!(
|
||||
persistence::decode(&serde_json::to_vec(&old).unwrap()).unwrap(),
|
||||
p
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source6_category_and_saved_selection_reidentify_delete_preserve_empty_filters() {
|
||||
let (p, exchange, category, diagram, a) = setup();
|
||||
let replacement = Id::new_v4();
|
||||
let q = execute_batch(&p, &[Command::Reidentify(exchange, replacement)]).unwrap();
|
||||
assert!(
|
||||
matches!(q.elements[&category].shared(),Some(SharedBody::ExchangeCategory{exchanges,..}) if exchanges==&[replacement])
|
||||
);
|
||||
assert_eq!(
|
||||
q.diagrams[&diagram].view_options.hidden_edges,
|
||||
vec![replacement]
|
||||
);
|
||||
let r = execute_batch(&q, &[Command::Delete(replacement)]).unwrap();
|
||||
assert!(r.contains(category));
|
||||
assert!(r.contains(a));
|
||||
assert!(r.diagrams[&diagram].view_options.hidden_edges.is_empty());
|
||||
assert!(
|
||||
matches!(r.elements[&category].shared(),Some(SharedBody::ExchangeCategory{exchanges,..}) if exchanges.is_empty())
|
||||
);
|
||||
let s = execute_batch(&r, &[Command::Delete(category)]).unwrap();
|
||||
assert_eq!(
|
||||
s.diagrams[&diagram].view_options.participant_categories,
|
||||
CategorySelection::Any(vec![])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source6_category_transition_requires_mapped_members_and_reviews_exact_set() {
|
||||
let (mut p, exchange, category, _, _) = setup();
|
||||
let sa = p
|
||||
.add_element(Kind::System, Layer::Sa, "System", None)
|
||||
.unwrap();
|
||||
let sb = p
|
||||
.add_element(Kind::SystemActor, Layer::Sa, "External", None)
|
||||
.unwrap();
|
||||
let port = |p: &mut Project, owner, name: &str| {
|
||||
let id = Id::new_v4();
|
||||
p.elements.insert(
|
||||
id,
|
||||
Element {
|
||||
id,
|
||||
alias: p.fresh_alias(name, "port", id),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
parent: Some(owner),
|
||||
body: ElementBody::shared_in(
|
||||
Layer::Sa,
|
||||
SharedBody::Port {
|
||||
contract: PortContract::Component {
|
||||
kind: ComponentPortKind::Standard,
|
||||
orientation: ComponentPortOrientation::Unset,
|
||||
},
|
||||
legacy: None,
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
id
|
||||
};
|
||||
let pa = port(&mut p, sa, "A endpoint");
|
||||
let pb = port(&mut p, sb, "B endpoint");
|
||||
let boundary = p.relate(RelationKind::BoundaryExchange, "Boundary", pa, pb);
|
||||
let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![category]);
|
||||
assert!(!plan_transition(&p, &request).unwrap().conflicts.is_empty());
|
||||
request.mappings.push(TransitionMapping {
|
||||
source: exchange,
|
||||
target: Some(boundary),
|
||||
target_kind: None,
|
||||
});
|
||||
let plan = plan_transition(&p, &request).unwrap();
|
||||
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
||||
let q = apply_transition(&p, &plan).unwrap();
|
||||
let trace = q.traces.values().find(|t| t.oa == category).unwrap();
|
||||
let target = trace.sa;
|
||||
assert!(
|
||||
matches!(q.elements[&target].shared(),Some(SharedBody::ExchangeCategory{family:ExchangeFamily::Participant,exchanges}) if exchanges==&[boundary])
|
||||
);
|
||||
let facets = semantic_facets(&q, trace, &BTreeMap::from([(exchange, boundary)])).unwrap();
|
||||
assert!(
|
||||
facets
|
||||
.iter()
|
||||
.any(|f| f.selector == FacetSelector::Category(CategoryFacet::Family))
|
||||
);
|
||||
assert!(facets.iter().any(|f| matches!(
|
||||
f.selector,
|
||||
FacetSelector::Member {
|
||||
collection: ReferenceCollection::CategoryExchanges,
|
||||
..
|
||||
}
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source6_new_historical_facts_are_rejected_under_source5_even_without_live_categories() {
|
||||
let mut p = Project::empty();
|
||||
let oa = p.add_element(Kind::Entity, Layer::Oa, "OA", None).unwrap();
|
||||
let sa = p
|
||||
.add_element(Kind::SystemActor, Layer::Sa, "SA", None)
|
||||
.unwrap();
|
||||
for fact in [
|
||||
FacetReview::Category(CategoryReview::Family(Reviewed {
|
||||
oa: Observed::NotApplicable,
|
||||
sa: Observed::NotApplicable,
|
||||
})),
|
||||
FacetReview::Member {
|
||||
collection: ReferenceCollection::CategoryExchanges,
|
||||
keys: KeyPair {
|
||||
oa: Some(Id::new_v4()),
|
||||
sa: None,
|
||||
},
|
||||
review: Reviewed {
|
||||
oa: Observed::NotApplicable,
|
||||
sa: Observed::NotApplicable,
|
||||
},
|
||||
},
|
||||
] {
|
||||
let id = Id::new_v4();
|
||||
p.traces.clear();
|
||||
p.traces.insert(
|
||||
id,
|
||||
Trace {
|
||||
id,
|
||||
alias: "review".into(),
|
||||
name: "History".into(),
|
||||
description: String::new(),
|
||||
oa,
|
||||
sa,
|
||||
kind: TraceKind::Manual,
|
||||
baseline: Some(TraceBaseline {
|
||||
oa: p.properties(oa).unwrap(),
|
||||
sa: p.properties(sa).unwrap(),
|
||||
semantic: Some(SemanticBaseline { facets: vec![fact] }),
|
||||
}),
|
||||
},
|
||||
);
|
||||
let mut json = serde_json::to_value(&p).unwrap();
|
||||
json["schema_version"] = 5.into();
|
||||
let error = persistence::decode(&serde_json::to_vec(&json).unwrap())
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(
|
||||
error.contains("category semantic facts require schema/source 6"),
|
||||
"{error}"
|
||||
);
|
||||
let old = language::format(&p).replacen("arcadia 6;", "arcadia 5;", 1);
|
||||
assert!(language::parse(&old).unwrap_err().iter().any(|d| {
|
||||
d.message
|
||||
.contains("category semantic facts require schema/source 6")
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source6_projection_filters_keep_placements_and_label_modes_are_presentation_only() {
|
||||
let (mut p, exchange, _, diagram, _) = setup();
|
||||
let hidden = projection::project_view(&p, diagram);
|
||||
assert!(hidden.edges.is_empty());
|
||||
p.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.view_options
|
||||
.hidden_edges
|
||||
.clear();
|
||||
let shown = projection::project_view(&p, diagram);
|
||||
assert_eq!(shown.edges.len(), 1);
|
||||
assert_eq!(shown.edges[0].id, exchange);
|
||||
assert_eq!(shown.nodes, hidden.nodes);
|
||||
assert_eq!(shown.edges[0].source_port, None);
|
||||
assert_eq!(shown.edges[0].target_port, None);
|
||||
assert!(!shown.edges[0].oriented);
|
||||
let route = routing::route_diagram(&p, &p.diagrams[&diagram], &shown);
|
||||
assert!(route.edges[&exchange].label.text.contains("No items"));
|
||||
p.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.view_options
|
||||
.relation_kinds = RelationKinds::Only(vec![]);
|
||||
assert!(projection::project_view(&p, diagram).edges.is_empty());
|
||||
}
|
||||
@@ -157,7 +157,17 @@ fn cross_file_typed_ports_global_forward_references_and_stable_ids() {
|
||||
assert_eq!(again.decoded.project, result.decoded.project);
|
||||
assert_eq!(again.bundle, result.bundle);
|
||||
assert_eq!(again.index, result.index);
|
||||
assert!(result.decoded.migration.is_none());
|
||||
let migration = result.decoded.migration.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
(migration.from_schema, migration.to_schema),
|
||||
(5, oa_core::SCHEMA_VERSION)
|
||||
);
|
||||
assert!(input.files[&input.entry].contains("arcadia 5;"));
|
||||
assert!(result.bundle.files[&result.bundle.entry].contains("arcadia 6;"));
|
||||
assert!(
|
||||
again.decoded.migration.is_none(),
|
||||
"stabilized current source must not migrate twice"
|
||||
);
|
||||
let project = &result.decoded.project;
|
||||
assert_eq!(
|
||||
project.relations[&id(project, "message")].source(),
|
||||
|
||||
@@ -18,6 +18,7 @@ fn view(p: &mut Project, machine: Id, alias: &str) -> Id {
|
||||
description: String::new(),
|
||||
layer: Layer::Oa,
|
||||
projection: ProjectionKind::StateMachine { machine },
|
||||
view_options: Default::default(),
|
||||
nodes: BTreeMap::new(),
|
||||
routes: vec![],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user