Files
ARCADIA-Studio/crates/oa-core/tests/r6_data.rs
T

1339 lines
40 KiB
Rust

use oa_core::{
data::*,
references,
transitions::*,
validation::{self, Severity},
*,
};
fn add(p: &mut Project, kind: Kind, name: &str, parent: Option<Id>) -> Id {
p.add_element(kind, Layer::Oa, name, parent).unwrap()
}
fn body(p: &mut Project, id: Id, body: SharedBody) {
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = body;
}
fn make_record(p: &mut Project, name: &str, supertype: Option<Id>) -> Id {
let id = add(p, Kind::DataType, name, None);
body(
p,
id,
SharedBody::DataType {
definition: DataTypeDefinition::Record { supertype },
default: None,
unit: String::new(),
},
);
id
}
fn field(
p: &mut Project,
owner: Id,
name: &str,
type_ref: TypeRef,
direction: ParameterDirection,
) -> Id {
let id = add(p, Kind::DataField, name, Some(owner));
body(
p,
id,
SharedBody::DataField {
type_ref,
minimum: 1,
maximum: Some(1),
unit: String::new(),
direction,
storage: FieldStorage::Value,
default: None,
},
);
id
}
fn edited(p: &Project, id: Id, change: impl FnOnce(&mut SharedBody)) -> Command {
let mut e = p.elements[&id].clone();
change(e.shared_mut().unwrap());
Command::EditElement(id, e)
}
fn valid(p: &Project) {
let errors: Vec<_> = validation::validate(p)
.into_iter()
.filter(|i| i.severity == Severity::Error)
.collect();
assert!(errors.is_empty(), "{errors:?}");
}
fn code(p: &Project, code: &str, severity: Severity) -> bool {
validation::validate(p)
.iter()
.any(|i| i.code == code && i.severity == severity)
}
fn values(p: &Project, id: Id) -> &Vec<MessageValue> {
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { values, .. },
..
}) = p.elements[&id].shared()
else {
panic!()
};
values
}
#[allow(clippy::too_many_arguments)]
fn message(
p: &mut Project,
scenario: Id,
exchange: Id,
kind: MessageKind,
reply_to: Option<Id>,
name: &str,
values: Vec<MessageValue>,
) -> Id {
let id = add(p, Kind::Occurrence, name, Some(scenario));
body(
p,
id,
SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
exchange: Some(exchange),
sender: None,
receiver: None,
message_kind: kind,
reply_to,
values,
},
precondition: String::new(),
postcondition: String::new(),
},
);
p.elements
.get_mut(&scenario)
.unwrap()
.steps_mut()
.unwrap()
.push(id);
id
}
struct Payload {
p: Project,
item: Id,
record: Id,
leaf: Id,
origin: Id,
destination: Id,
exchange: Id,
call: Id,
reply: Id,
}
fn payload() -> Payload {
let mut p = Project::empty();
let item = add(&mut p, Kind::ExchangeItem, "Move", None);
body(
&mut p,
item,
SharedBody::ExchangeItem {
kind: ExchangeItemKind::Information,
mechanism: ExchangeMechanism::Operation,
supertype: None,
},
);
let record = make_record(&mut p, "Coordinate", None);
let leaf = field(
&mut p,
record,
"latitude",
TypeRef::Real,
ParameterDirection::Unspecified,
);
let origin = field(
&mut p,
item,
"origin",
TypeRef::Named(record),
ParameterDirection::In,
);
let destination = field(
&mut p,
item,
"destination",
TypeRef::Named(record),
ParameterDirection::Out,
);
let a = add(&mut p, Kind::Activity, "Request movement", None);
let b = add(&mut p, Kind::Activity, "Perform movement", None);
let exchange = p.relate(RelationKind::Interaction, "Movement invocation", a, b);
if let RelationBody::Interaction { items, .. } =
&mut p.relations.get_mut(&exchange).unwrap().body
{
items.push(item);
}
let scenario = add(&mut p, Kind::Scenario, "Movement conversation", None);
let call = message(
&mut p,
scenario,
exchange,
MessageKind::Call,
None,
"Call",
vec![MessageValue {
path: vec![origin],
field: leaf,
expression: "45.0".into(),
}],
);
let reply = message(
&mut p,
scenario,
exchange,
MessageKind::Reply,
Some(call),
"Reply",
vec![MessageValue {
path: vec![destination],
field: leaf,
expression: "46.0".into(),
}],
);
valid(&p);
Payload {
p,
item,
record,
leaf,
origin,
destination,
exchange,
call,
reply,
}
}
#[test]
fn scalar_bounds_defaults_unicode_units_and_failed_batches_are_exact() {
let mut p = Project::empty();
let integer = add(&mut p, Kind::DataType, "Full precision integer", None);
body(
&mut p,
integer,
SharedBody::DataType {
definition: DataTypeDefinition::Integer {
minimum: Some(i64::MAX - 1),
maximum: Some(i64::MAX),
},
default: Some(DataValue::Integer(i64::MAX)),
unit: "count".into(),
},
);
let text = add(&mut p, Kind::DataType, "Two scalars", None);
body(
&mut p,
text,
SharedBody::DataType {
definition: DataTypeDefinition::String {
minimum_length: 2,
maximum_length: Some(2),
},
default: Some(DataValue::String("é🙂".into())),
unit: String::new(),
},
);
let real = add(&mut p, Kind::DataType, "Mass flow", None);
body(
&mut p,
real,
SharedBody::DataType {
definition: DataTypeDefinition::Real {
minimum: Some(0.0),
maximum: Some(25.0),
},
default: Some(DataValue::Real(1.0)),
unit: "kg/s".into(),
},
);
let item = add(&mut p, Kind::ExchangeItem, "Fuel", None);
body(
&mut p,
item,
SharedBody::ExchangeItem {
kind: ExchangeItemKind::Material,
mechanism: ExchangeMechanism::Flow,
supertype: None,
},
);
let amount = field(
&mut p,
item,
"amount",
TypeRef::Named(real),
ParameterDirection::Unspecified,
);
assert_eq!(effective_field_unit(&p, &p.elements[&amount]), Some("kg/s"));
assert_eq!(
effective_element_default(&p, &p.elements[&amount]),
Some(&DataValue::Real(1.0))
);
valid(&p);
assert!(
validate_data_value(
&p,
&TypeRef::Named(integer),
&DataValue::Integer(i64::MAX - 2)
)
.is_err()
);
assert!(validate_data_value(&p, &TypeRef::Named(real), &DataValue::Integer(1)).is_err());
assert!(validate_data_value(&p, &TypeRef::Real, &DataValue::Real(f64::INFINITY)).is_err());
assert!(
validate_data_value(&p, &TypeRef::Named(text), &DataValue::String("é".into())).is_err()
);
let mut session = Session::new(p.clone());
let invalid = edited(&p, integer, |s| {
if let SharedBody::DataType { default, .. } = s {
*default = Some(DataValue::Integer(i64::MIN));
}
});
assert!(
session
.execute(Command::Batch(vec![
Command::Rename(item, "Unsaved name".into()),
invalid
]))
.is_err()
);
assert_eq!(session.project(), &p);
assert_eq!(session.revision(), 0);
assert!(!session.can_undo());
let invalid = edited(&p, real, |s| {
if let SharedBody::DataType { definition, .. } = s {
*definition = DataTypeDefinition::Real {
minimum: Some(f64::NAN),
maximum: None,
};
}
});
assert!(session.execute(invalid).is_err());
let empty = edited(&p, amount, |s| {
if let SharedBody::DataField {
minimum, default, ..
} = s
{
*minimum = 0;
*default = Some(vec![]);
}
});
session.execute(empty).unwrap();
valid(session.project());
session.undo();
assert_eq!(session.project(), &p);
}
#[test]
fn enum_literal_edits_validate_type_and_field_defaults_in_one_candidate() {
let mut p = Project::empty();
let enumeration = add(&mut p, Kind::DataType, "Mode", None);
body(
&mut p,
enumeration,
SharedBody::DataType {
definition: DataTypeDefinition::Enumeration {
literals: vec!["Ready".into(), "Busy".into()],
},
default: Some(DataValue::Enumeration("Ready".into())),
unit: String::new(),
},
);
let item = add(&mut p, Kind::ExchangeItem, "Status", None);
let mode = field(
&mut p,
item,
"mode",
TypeRef::Named(enumeration),
ParameterDirection::Unspecified,
);
if let Some(SharedBody::DataField { default, .. }) =
p.elements.get_mut(&mode).unwrap().shared_mut()
{
*default = Some(vec![DataValue::Enumeration("Ready".into())]);
}
let mut session = Session::new(p.clone());
let change_type = edited(&p, enumeration, |s| {
if let SharedBody::DataType {
definition: DataTypeDefinition::Enumeration { literals },
default,
..
} = s
{
literals[0] = "Idle".into();
*default = Some(DataValue::Enumeration("Idle".into()));
}
});
assert!(session.execute(change_type.clone()).is_err());
assert_eq!(session.project(), &p);
let change_field = edited(&p, mode, |s| {
if let SharedBody::DataField { default, .. } = s {
*default = Some(vec![DataValue::Enumeration("Idle".into())]);
}
});
session
.execute(Command::Batch(vec![change_type, change_field]))
.unwrap();
valid(session.project());
session.undo();
assert_eq!(session.project(), &p);
}
#[test]
fn an_empty_enumeration_is_a_located_incomplete_draft() {
let mut p = Project::empty();
let enumeration = add(&mut p, Kind::DataType, "Pending values", None);
body(
&mut p,
enumeration,
SharedBody::DataType {
definition: DataTypeDefinition::Enumeration { literals: vec![] },
default: None,
unit: String::new(),
},
);
valid(&p);
assert!(
validation::validate(&p)
.iter()
.any(|issue| issue.code == "empty_enumeration"
&& issue.severity == Severity::Warning
&& issue.element == Some(enumeration)
&& issue.field.as_deref() == Some("literals"))
);
Session::try_new(p).unwrap();
}
#[test]
fn inherited_interfaces_and_item_specializations_satisfy_typed_port_contracts() {
let mut p = Project::empty();
let a = p
.add_element(Kind::Function, Layer::Sa, "Send", None)
.unwrap();
let b = p
.add_element(Kind::Function, Layer::Sa, "Receive", None)
.unwrap();
let ca = p
.add_element(Kind::System, Layer::Sa, "Client", None)
.unwrap();
let cb = p
.add_element(Kind::SystemActor, Layer::Sa, "Provider", None)
.unwrap();
p.relate(RelationKind::Allocation, "Client work", ca, a);
p.relate(RelationKind::Allocation, "Provider work", cb, b);
let item = p
.add_element(Kind::ExchangeItem, Layer::Sa, "Base item", None)
.unwrap();
let specialized = p
.add_element(Kind::ExchangeItem, Layer::Sa, "Specialized item", None)
.unwrap();
body(
&mut p,
specialized,
SharedBody::ExchangeItem {
kind: ExchangeItemKind::Information,
mechanism: ExchangeMechanism::Unspecified,
supertype: Some(item),
},
);
let fe = Id::new_v4();
let request = FunctionalExchangeRequest {
id: fe,
alias: "specialized_exchange".into(),
name: "Specialized exchange".into(),
description: String::new(),
source: a,
target: b,
source_port: PortChoice::Create,
target_port: PortChoice::Create,
items: vec![item],
};
p = execute_batch(&p, &plan_functional_exchange(&p, &request).unwrap()).unwrap();
let mut changed = p.relations[&fe].clone();
if let RelationBody::FunctionalExchange { items, .. } = &mut changed.body {
*items = vec![specialized];
}
p = execute_batch(&p, &[Command::EditRelation(fe, changed)]).unwrap();
let boundary = Id::new_v4();
let request = ComponentExchangeRequest {
functional_exchange: fe,
boundary: ComponentExchangeChoice::Create {
id: boundary,
alias: "service_boundary".into(),
name: "Service boundary".into(),
description: String::new(),
port_kind: ComponentPortKind::Standard,
oriented: false,
source_port: PortChoice::Create,
target_port: PortChoice::Create,
},
};
p = execute_batch(&p, &plan_component_exchange(&p, &request).unwrap()).unwrap();
let interface = p
.add_element(Kind::Interface, Layer::Sa, "Service", None)
.unwrap();
let extended = p
.add_element(Kind::Interface, Layer::Sa, "Extended service", None)
.unwrap();
body(
&mut p,
extended,
SharedBody::Interface {
supertype: Some(interface),
},
);
p.relate(
RelationKind::InterfaceItemAllocation,
"Service item",
interface,
item,
);
let (source, target) = p.relations[&boundary].body.endpoints();
p.relate(
RelationKind::InterfaceRequirement,
"Required service",
source,
interface,
);
let provider = p.relate(
RelationKind::InterfaceProvision,
"Provided service",
target,
extended,
);
valid(&p);
assert!(!code(
&p,
"unsatisfied_interface_requirement",
Severity::Warning
));
let unrelated = p
.add_element(Kind::Interface, Layer::Sa, "Unrelated service", None)
.unwrap();
p.relate(
RelationKind::InterfaceItemAllocation,
"Other item contract",
unrelated,
item,
);
p.relations
.get_mut(&provider)
.unwrap()
.body
.set_endpoints(target, unrelated);
valid(&p);
assert!(code(
&p,
"unsatisfied_interface_requirement",
Severity::Warning
));
}
#[test]
fn repeated_record_paths_enforce_call_reply_phase_and_survive_identity_changes() {
let Payload {
p,
item,
record,
leaf,
origin,
destination,
exchange,
call,
reply,
} = payload();
let paths = carried_field_paths(&p, &p.relations[&exchange]);
assert_eq!(paths.iter().filter(|f| f.field == leaf).count(), 2);
let mut session = Session::new(p.clone());
let wrong = edited(&p, call, |s| {
if let SharedBody::Occurrence {
body: OccurrenceBody::Exchange { values, .. },
..
} = s
{
values[0].path = vec![destination];
}
});
assert!(
session
.execute(wrong)
.unwrap_err()
.0
.contains("message_parameter_phase")
);
assert_eq!(session.project(), &p);
let new_origin = Id::new_v4();
let new_leaf = Id::new_v4();
session
.execute(Command::Batch(vec![
Command::Reidentify(origin, new_origin),
Command::Reidentify(leaf, new_leaf),
]))
.unwrap();
assert_eq!(values(session.project(), call)[0].path, vec![new_origin]);
assert_eq!(values(session.project(), call)[0].field, new_leaf);
assert_eq!(values(session.project(), reply)[0].field, new_leaf);
session.undo();
assert_eq!(session.project(), &p);
session.execute(Command::Delete(origin)).unwrap();
assert!(values(session.project(), call).is_empty());
assert_eq!(values(session.project(), reply).len(), 1);
assert!(session.project().elements.contains_key(&record));
assert!(session.project().elements.contains_key(&leaf));
session.undo();
assert_eq!(session.project(), &p);
let reference = edited(&p, origin, |s| {
if let SharedBody::DataField { storage, .. } = s {
*storage = FieldStorage::Reference;
}
});
assert!(
session
.execute(reference)
.unwrap_err()
.0
.contains("message_value_field")
);
let direction = edited(&p, origin, |s| {
if let SharedBody::DataField { direction, .. } = s {
*direction = ParameterDirection::Unspecified;
}
});
let generic = edited(&p, item, |s| {
if let SharedBody::ExchangeItem { mechanism, .. } = s {
*mechanism = ExchangeMechanism::Flow;
}
});
assert!(
session
.execute(Command::Batch(vec![direction, generic]))
.is_err()
);
}
#[test]
fn ambiguous_legacy_annotations_warn_and_reference_fields_stop_payload_descent() {
let Payload {
mut p,
origin,
destination,
call,
reply,
leaf,
exchange,
..
} = payload();
for message in [call, reply] {
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { values, .. },
..
}) = p.elements.get_mut(&message).unwrap().shared_mut()
{
values[0].path.clear();
}
}
valid(&p);
assert!(code(&p, "message_value_ambiguous_path", Severity::Warning));
if let Some(SharedBody::DataField { storage, .. }) =
p.elements.get_mut(&origin).unwrap().shared_mut()
{
*storage = FieldStorage::Reference;
}
let paths = carried_field_paths(&p, &p.relations[&exchange]);
assert!(paths.iter().any(|f| f.field == origin && f.path.is_empty()));
assert_eq!(
paths
.iter()
.filter(|f| f.field == leaf)
.map(|f| &f.path)
.collect::<Vec<_>>(),
vec![&vec![destination]]
);
}
#[test]
fn identified_allocations_keep_protocols_order_artifacts_and_inherited_identity() {
let mut p = Project::empty();
let item = add(&mut p, Kind::ExchangeItem, "Telemetry", None);
let other_item = add(&mut p, Kind::ExchangeItem, "Command", None);
let first = add(&mut p, Kind::Interface, "Primary", None);
let second = add(&mut p, Kind::Interface, "Secondary", None);
let derived = add(&mut p, Kind::Interface, "Specialized primary", None);
body(
&mut p,
derived,
SharedBody::Interface {
supertype: Some(first),
},
);
let a = p.relate(
RelationKind::InterfaceItemAllocation,
"Primary telemetry",
first,
item,
);
let b = p.relate(
RelationKind::InterfaceItemAllocation,
"Secondary telemetry",
second,
item,
);
if let RelationBody::InterfaceItemAllocation {
transmission,
acquisition,
..
} = &mut p.relations.get_mut(&a).unwrap().body
{
*transmission = TransmissionProtocol::Synchronous;
*acquisition = AcquisitionProtocol::Read;
}
if let RelationBody::InterfaceItemAllocation {
transmission,
order,
..
} = &mut p.relations.get_mut(&b).unwrap().body
{
*transmission = TransmissionProtocol::Broadcast;
*order = 4;
}
let local = p.relate(
RelationKind::InterfaceItemAllocation,
"Local command",
derived,
other_item,
);
assert_eq!(
effective_interface_allocations(&p, derived)
.iter()
.map(|r| r.id)
.collect::<Vec<_>>(),
vec![a, local]
);
assert!(interface_satisfies(&p, derived, first));
assert!(!interface_satisfies(&p, first, derived));
let artifact_id = Id::new_v4();
p.artifact_links.insert(
artifact_id,
ArtifactLink {
id: artifact_id,
alias: "allocation_spec".into(),
name: "Protocol specification".into(),
description: String::new(),
owner: a,
relation: ArtifactRelation::References,
uri: "https://example.org/protocol".into(),
revision: None,
fragment: None,
},
);
valid(&p);
let mut session = Session::new(p.clone());
let duplicate = Relation {
id: Id::new_v4(),
alias: "duplicate_allocation".into(),
name: "Duplicate inherited item".into(),
description: String::new(),
body: RelationBody::new(RelationKind::InterfaceItemAllocation, derived, item),
};
assert!(session.execute(Command::CreateRelation(duplicate)).is_err());
let impact = references::deletion_impact(&p, a);
assert!(impact.contains(&artifact_id));
assert!(!impact.contains(&item));
assert!(!impact.contains(&b));
session.execute(Command::Delete(a)).unwrap();
assert_eq!(
interface_items(session.project(), derived),
vec![other_item]
);
assert_eq!(session.project().relations[&b], p.relations[&b]);
assert!(session.project().elements.contains_key(&item));
session.undo();
assert_eq!(session.project(), &p);
let new_item = Id::new_v4();
session
.execute(Command::Reidentify(item, new_item))
.unwrap();
assert_eq!(session.project().relations[&a].target(), new_item);
assert_eq!(session.project().relations[&b].target(), new_item);
}
#[test]
fn specialization_preserves_fields_rejects_cycles_and_prunes_removed_inherited_paths() {
let Payload {
mut p,
item,
record,
leaf,
origin,
call,
reply,
..
} = payload();
let specialized = make_record(&mut p, "Refined coordinate", Some(record));
if let Some(SharedBody::DataField { type_ref, .. }) =
p.elements.get_mut(&origin).unwrap().shared_mut()
{
*type_ref = TypeRef::Named(specialized);
}
valid(&p);
assert_eq!(effective_fields(&p, specialized)[0].id, leaf);
assert_eq!(p.elements[&leaf].parent, Some(record));
let mut session = Session::new(p.clone());
let unbase = edited(&p, specialized, |s| {
if let SharedBody::DataType {
definition: DataTypeDefinition::Record { supertype },
..
} = s
{
*supertype = None;
}
});
let mut explicit = p.clone();
if let Command::EditElement(id, e) = &unbase {
explicit.elements.insert(*id, e.clone());
}
assert!(
session
.execute(Command::Replace(explicit))
.unwrap_err()
.0
.contains("message_value_field")
);
assert_eq!(session.project(), &p);
session.execute(unbase).unwrap();
assert!(values(session.project(), call).is_empty());
assert_eq!(values(session.project(), reply).len(), 1);
session.undo();
assert_eq!(session.project(), &p);
let cycle = edited(&p, record, |s| {
if let SharedBody::DataType {
definition: DataTypeDefinition::Record { supertype },
..
} = s
{
*supertype = Some(specialized);
}
});
assert!(
session
.execute(cycle)
.unwrap_err()
.0
.contains("data_inheritance_cycle")
);
let shadow = field(
&mut p,
specialized,
"latitude",
TypeRef::Real,
ParameterDirection::Unspecified,
);
assert!(code(&p, "inherited_field_shadow", Severity::Error));
p.elements.remove(&shadow);
let derived_item = add(&mut p, Kind::ExchangeItem, "Precise move", None);
body(
&mut p,
derived_item,
SharedBody::ExchangeItem {
kind: ExchangeItemKind::Information,
mechanism: ExchangeMechanism::Operation,
supertype: Some(item),
},
);
assert!(oa_core::item_satisfies(&p, derived_item, item));
assert!(!oa_core::item_satisfies(&p, item, derived_item));
let result = field(
&mut p,
item,
"result",
TypeRef::Boolean,
ParameterDirection::Return,
);
field(
&mut p,
derived_item,
"second result",
TypeRef::Boolean,
ParameterDirection::Return,
);
assert!(code(&p, "multiple_return_parameters", Severity::Error));
assert_eq!(p.elements[&result].parent, Some(item));
}
#[test]
fn removing_inheritance_prunes_legacy_annotations_only_when_every_reachable_path_disappears() {
let Payload {
mut p,
record,
origin,
destination,
call,
reply,
..
} = payload();
let derived = make_record(&mut p, "Inherited arguments", Some(record));
for id in [origin, destination] {
if let Some(SharedBody::DataField { type_ref, .. }) =
p.elements.get_mut(&id).unwrap().shared_mut()
{
*type_ref = TypeRef::Named(derived);
}
}
for id in [call, reply] {
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { values, .. },
..
}) = p.elements.get_mut(&id).unwrap().shared_mut()
{
values[0].path.clear();
}
}
valid(&p);
assert!(code(&p, "message_value_ambiguous_path", Severity::Warning));
let remove = edited(&p, derived, |s| {
if let SharedBody::DataType {
definition: DataTypeDefinition::Record { supertype },
..
} = s
{
*supertype = None;
}
});
let mut session = Session::new(p.clone());
session.execute(remove).unwrap();
assert!(values(session.project(), call).is_empty());
assert!(values(session.project(), reply).is_empty());
assert!(session.project().elements.contains_key(&record));
session.undo();
assert_eq!(session.project(), &p);
}
#[test]
fn recursive_value_types_warn_while_optional_reference_cycles_and_associations_are_distinct() {
let mut p = Project::empty();
let a = make_record(&mut p, "Node", None);
let b = make_record(&mut p, "Envelope", None);
let next = field(
&mut p,
a,
"next",
TypeRef::Named(a),
ParameterDirection::Unspecified,
);
field(
&mut p,
b,
"root",
TypeRef::Named(a),
ParameterDirection::Unspecified,
);
valid(&p);
assert!(code(&p, "record_no_finite_value", Severity::Warning));
if let Some(SharedBody::DataField { minimum, .. }) =
p.elements.get_mut(&next).unwrap().shared_mut()
{
*minimum = 0;
}
assert!(!code(&p, "record_no_finite_value", Severity::Warning));
if let Some(SharedBody::DataField {
minimum, storage, ..
}) = p.elements.get_mut(&next).unwrap().shared_mut()
{
*minimum = 1;
*storage = FieldStorage::Reference;
}
assert!(!code(&p, "record_no_finite_value", Severity::Warning));
let association = p.relate(RelationKind::DataAssociation, "Parent to children", a, a);
if let RelationBody::DataAssociation {
source_role,
target_role,
source_navigable,
target_navigable,
..
} = &mut p.relations.get_mut(&association).unwrap().body
{
*source_role = "parent".into();
*target_role = "children".into();
*source_navigable = true;
*target_navigable = true;
}
valid(&p);
let mut duplicate = p.relations[&association].clone();
duplicate.id = Id::new_v4();
duplicate.alias = "second_role_contract".into();
p.relations.insert(duplicate.id, duplicate);
valid(&p);
assert!(code(&p, "duplicate_association_roles", Severity::Warning));
let mut session = Session::new(p.clone());
let mut invalid = p.relations[&association].clone();
if let RelationBody::DataAssociation { target_role, .. } = &mut invalid.body {
*target_role = "parent".into();
}
assert!(
session
.execute(Command::EditRelation(association, invalid))
.is_err()
);
session.execute(Command::Delete(association)).unwrap();
assert!(session.project().elements.contains_key(&a));
assert!(session.project().elements.contains_key(&next));
}
#[test]
fn a_self_association_is_visible_with_neutral_connector_and_real_role_contracts() {
let mut p = Project::empty();
let record = make_record(&mut p, "Tree node", None);
let relation = p.relate(
RelationKind::DataAssociation,
"Parent-child reference",
record,
record,
);
if let RelationBody::DataAssociation {
source_role,
target_role,
source_maximum,
target_navigable,
..
} = &mut p.relations.get_mut(&relation).unwrap().body
{
*source_role = "parent".into();
*target_role = "children".into();
*source_maximum = Some(1);
*target_navigable = true;
}
let diagram = p.ensure_diagram(Layer::Oa);
p.place(diagram, record, Position { x: 100., y: 100. });
let item = add(
&mut p,
Kind::ExchangeItem,
"Nested allocation payload",
None,
);
let interface = add(&mut p, Kind::Interface, "Nested allocation owner", None);
let allocation = p.relate(
RelationKind::InterfaceItemAllocation,
"Nested contract row",
interface,
item,
);
p.place(diagram, item, Position { x: 600., y: 100. });
p.place(diagram, interface, Position { x: 600., y: 300. });
let view = oa_core::projection::project_view(&p, diagram);
assert!(!view.edges.iter().any(|e| e.id == allocation));
let projected = view.edges.iter().find(|e| e.id == relation).unwrap();
assert_eq!(projected.source, projected.target);
assert!(!projected.oriented);
let routed = oa_core::routing::route_diagram(&p, &p.diagrams[&diagram], &view);
let edge = &routed.edges[&relation];
assert!(edge.points.len() > 3);
assert_ne!(edge.points.first(), edge.points.last());
assert_eq!(edge.label.text, "Parent-child reference");
assert_eq!(
association_end_label(&p.relations[&relation], true).unwrap(),
"parent [0..1]"
);
assert_eq!(
association_end_label(&p.relations[&relation], false).unwrap(),
"children [0..*]"
);
let ends = oa_core::routing::association_end_geometry(&p.relations[&relation], edge);
assert_eq!(ends.len(), 2);
assert!(ends[0].source_end);
assert!(!ends[0].navigable);
assert!(ends[1].navigable);
assert_eq!(ends[0].tip, *edge.points.first().unwrap());
assert_eq!(ends[1].tip, *edge.points.last().unwrap());
assert_ne!(ends[0].label.center, ends[1].label.center);
for end in &ends {
let b = end.label.bounds;
assert!(
b.position.x >= routed.bounds.position.x && b.position.y >= routed.bounds.position.y
);
assert!(
b.position.x + b.size.width
<= routed.bounds.position.x + routed.bounds.size.width + 0.01
);
assert!(
b.position.y + b.size.height
<= routed.bounds.position.y + routed.bounds.size.height + 0.01
);
}
let mut changed = p.relations[&relation].clone();
if let RelationBody::DataAssociation {
source_navigable,
target_navigable,
..
} = &mut changed.body
{
*source_navigable = true;
*target_navigable = false;
}
let changed_ends = oa_core::routing::association_end_geometry(&changed, edge);
assert!(changed_ends[0].navigable);
assert!(!changed_ends[1].navigable);
assert_eq!(changed_ends[0].tip, ends[0].tip);
assert_eq!(changed_ends[1].tip, ends[1].tip);
for points in [
vec![Position { x: 0., y: 0. }, Position { x: 12., y: 0. }],
vec![
Position { x: 0., y: 0. },
Position { x: 18., y: 0. },
Position { x: 18., y: 5. },
Position { x: 0., y: 5. },
],
] {
let mut short = edge.clone();
short.points = points;
let labels = oa_core::routing::association_end_geometry(&changed, &short);
let a = labels[0].label.bounds;
let b = labels[1].label.bounds;
assert!(
a.position.x + a.size.width <= b.position.x
|| b.position.x + b.size.width <= a.position.x
|| a.position.y + a.size.height <= b.position.y
|| b.position.y + b.size.height <= a.position.y,
"End labels overlap on a short or self route: {labels:?}"
);
}
}
#[test]
fn empty_record_collections_are_supported_but_record_literals_and_reference_defaults_are_not() {
let mut p = Project::empty();
let record = make_record(&mut p, "Collection member", None);
let item = add(&mut p, Kind::ExchangeItem, "Collection", None);
let collection = field(
&mut p,
item,
"members",
TypeRef::Named(record),
ParameterDirection::Unspecified,
);
if let Some(SharedBody::DataField {
minimum,
maximum,
default,
..
}) = p.elements.get_mut(&collection).unwrap().shared_mut()
{
*minimum = 0;
*maximum = None;
*default = Some(vec![]);
}
valid(&p);
let mut session = Session::new(p.clone());
let reference = edited(&p, collection, |s| {
if let SharedBody::DataField { storage, .. } = s {
*storage = FieldStorage::Reference;
}
});
assert!(
session
.execute(reference)
.unwrap_err()
.0
.contains("reference_field_contract")
);
let literal = edited(&p, collection, |s| {
if let SharedBody::DataField { default, .. } = s {
*default = Some(vec![DataValue::String("{}".into())]);
}
});
assert!(session.execute(literal).is_err());
assert_eq!(session.project(), &p);
}
#[test]
fn exponential_payload_expansion_is_reported_and_recursive_finite_paths_remain_valid() {
let mut p = Project::empty();
let item = add(&mut p, Kind::ExchangeItem, "Bounded payload", None);
let mut owner = item;
for depth in 0..15 {
let next = make_record(&mut p, &format!("Level {depth}"), None);
field(
&mut p,
owner,
"left",
TypeRef::Named(next),
ParameterDirection::Unspecified,
);
field(
&mut p,
owner,
"right",
TypeRef::Named(next),
ParameterDirection::Unspecified,
);
owner = next;
}
let a = add(&mut p, Kind::Activity, "A", None);
let b = add(&mut p, Kind::Activity, "B", None);
let exchange = p.relate(RelationKind::Interaction, "Expanded exchange", a, b);
if let RelationBody::Interaction { items, .. } =
&mut p.relations.get_mut(&exchange).unwrap().body
{
items.push(item);
}
assert!(try_carried_field_paths(&p, &p.relations[&exchange]).is_err());
assert!(code(&p, "data_path_expansion_limit", Severity::Error));
let mut p = Project::empty();
let item = add(&mut p, Kind::ExchangeItem, "Recursive payload", None);
let record = make_record(&mut p, "Node", None);
let root = field(
&mut p,
item,
"root",
TypeRef::Named(record),
ParameterDirection::Unspecified,
);
let next = field(
&mut p,
record,
"next",
TypeRef::Named(record),
ParameterDirection::Unspecified,
);
if let Some(SharedBody::DataField { minimum, .. }) =
p.elements.get_mut(&next).unwrap().shared_mut()
{
*minimum = 0;
}
let a = add(&mut p, Kind::Activity, "A", None);
let b = add(&mut p, Kind::Activity, "B", None);
let exchange = p.relate(RelationKind::Interaction, "Recursive exchange", a, b);
if let RelationBody::Interaction { items, .. } =
&mut p.relations.get_mut(&exchange).unwrap().body
{
items.push(item);
}
valid(&p);
assert!(try_carried_field_paths(&p, &p.relations[&exchange]).is_ok());
assert!(carried_value_path(&p, &p.relations[&exchange], &[root, next, next], next).is_some());
assert_eq!(
resolve_legacy_field_path(&p, &p.relations[&exchange], next).unwrap(),
LegacyFieldPathResolution::Ambiguous
);
let scenario = add(&mut p, Kind::Scenario, "Recursive annotation", None);
message(
&mut p,
scenario,
exchange,
MessageKind::Asynchronous,
None,
"Legacy recursive value",
vec![MessageValue {
path: vec![],
field: next,
expression: "descriptive annotation".into(),
}],
);
valid(&p);
assert!(code(&p, "message_value_ambiguous_path", Severity::Warning));
}
#[test]
fn initial_transition_copies_qualified_paths_inheritance_allocations_and_associations() {
let Payload {
mut p,
item,
record,
origin,
leaf,
call,
..
} = payload();
let base = make_record(&mut p, "Reference base", None);
if let Some(SharedBody::DataType {
definition: DataTypeDefinition::Record { supertype },
..
}) = p.elements.get_mut(&record).unwrap().shared_mut()
{
*supertype = Some(base);
}
let interface = add(&mut p, Kind::Interface, "Movement service", None);
let allocation = p.relate(
RelationKind::InterfaceItemAllocation,
"Service operation",
interface,
item,
);
let association = p.relate(
RelationKind::DataAssociation,
"Coordinate relation",
record,
base,
);
let scenario = p.elements[&call].parent.unwrap();
let mut request =
TransitionRequest::new(TransitionDirection::OaToSa, vec![scenario, interface]);
request.scope = DependencyScope::Required;
let plan = plan_transition(&p, &request).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let candidate = apply_transition(&p, &plan).unwrap();
valid(&candidate);
let mapped = |id| plan.changes.iter().find(|c| c.source == id).unwrap().target;
assert_eq!(
values(&candidate, mapped(call))[0].path,
vec![mapped(origin)]
);
assert_eq!(values(&candidate, mapped(call))[0].field, mapped(leaf));
assert_eq!(
candidate.relations[&mapped(allocation)].source(),
mapped(interface)
);
assert_eq!(
candidate.relations[&mapped(association)].target(),
mapped(base)
);
assert_eq!(
supertype(&candidate.elements[&mapped(record)]),
Some(mapped(base))
);
assert_eq!(p.elements[&leaf].parent, Some(record));
}
#[test]
fn required_dictionary_closure_does_not_expand_through_incident_associations() {
let mut p = Project::empty();
let a = make_record(&mut p, "A", None);
let b = make_record(&mut p, "B", None);
let c = make_record(&mut p, "C", None);
let ab = p.relate(RelationKind::DataAssociation, "A to B", a, b);
let bc = p.relate(RelationKind::DataAssociation, "B to C", b, c);
for (sources, expected, omitted) in [
(vec![a], vec![a], vec![b, c, ab, bc]),
(vec![a, b], vec![a, b, ab], vec![c, bc]),
(vec![ab], vec![a, b, ab], vec![c, bc]),
] {
let mut request = TransitionRequest::new(TransitionDirection::OaToSa, sources);
request.scope = DependencyScope::Required;
let plan = plan_transition(&p, &request).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let mapped: std::collections::BTreeSet<_> =
plan.changes.iter().map(|change| change.source).collect();
assert_eq!(mapped, expected.into_iter().collect());
for id in omitted {
assert!(!mapped.contains(&id));
}
valid(&apply_transition(&p, &plan).unwrap());
}
}
#[test]
#[ignore = "explicit inheritance scaling probe; run with --ignored --nocapture"]
fn inheritance_chain_validation_scaling_probe() {
for count in [250, 500, 1000] {
let mut p = Project::empty();
let mut base = None;
for index in 0..count {
base = Some(make_record(&mut p, &format!("Record {index}"), base));
}
let started = std::time::Instant::now();
valid(&p);
eprintln!(
"R6 inheritance validation {count} records: {:.3} ms",
started.elapsed().as_secs_f64() * 1000.0
);
}
}