Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
1340 lines
40 KiB
Rust
1340 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 {
|
|
operation: None,
|
|
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
|
|
);
|
|
}
|
|
}
|