Migrate frozen data contracts and authored source to schema 4

This commit is contained in:
2026-09-06 03:39:02 +03:00
parent c5276b1943
commit 1230ab8a3e
21 changed files with 2006 additions and 126 deletions
+38
View File
@@ -0,0 +1,38 @@
// Frozen authored schema3 compatibility input. Do not rewrite as current syntax.
arcadia 3;
project legacy_data "Legacy data contract" id "00000000-0000-0000-0000-000000000001" {
sa_function produce "Produce" id "00000000-0000-0000-0000-000000000002" {}
sa_function consume "Consume" id "00000000-0000-0000-0000-000000000003" {}
sa_exchange_item packet "Position event" id "00000000-0000-0000-0000-000000000004" {
kind Event;
// Reused record parameters retain their own identities.
sa_data_field origin "Origin" id "00000000-0000-0000-0000-000000000007" { type_ref { Named @coordinate; }; }
sa_data_field destination "Destination" id "00000000-0000-0000-0000-000000000008" { type_ref { Named @coordinate; }; }
}
sa_data_type coordinate "Coordinate" id "00000000-0000-0000-0000-000000000005" {
definition Record;
sa_data_field latitude "Latitude" id "00000000-0000-0000-0000-000000000006" { type_ref Real; unit "deg"; }
}
sa_interface sender_contract "Sender contract" id "00000000-0000-0000-0000-000000000009" { items [@packet]; }
sa_interface receiver_contract "Receiver contract" id "00000000-0000-0000-0000-00000000000a" { items [@packet]; }
sa_port output "Output" id "00000000-0000-0000-0000-00000000000b" {
parent @produce; contract { FunctionOutput { outgoing_items [@packet]; }; }; legacy none;
}
sa_port input "Input" id "00000000-0000-0000-0000-00000000000c" {
parent @consume; contract { FunctionInput { incoming_items [@packet]; }; }; legacy none;
}
functional_exchange position_exchange "Position exchange" id "00000000-0000-0000-0000-00000000000d" {
source @produce; target @consume; source_port @output; target_port @input; items [@packet];
}
sa_scenario scenario "Position scenario" id "00000000-0000-0000-0000-00000000000e" {
viewpoint Functional; steps [@message];
sa_lifeline sender "Sender" id "00000000-0000-0000-0000-00000000000f" { represented @produce; }
sa_lifeline receiver "Receiver" id "00000000-0000-0000-0000-000000000010" { represented @consume; }
sa_occurrence message "Position" id "00000000-0000-0000-0000-000000000011" {
body { Exchange {
exchange @position_exchange; sender @sender; receiver @receiver;
values [{ field @latitude; expression "navigation.latitude"; }];
}; };
}
}
}
+312
View File
@@ -0,0 +1,312 @@
{
"schema_version": 3,
"id": "00000000-0000-0000-0000-000000000001",
"alias": "legacy_data",
"name": "Legacy data contract",
"description": "",
"elements": {
"00000000-0000-0000-0000-000000000002": {
"id": "00000000-0000-0000-0000-000000000002",
"alias": "produce",
"name": "Produce",
"description": "",
"parent": null,
"body": {
"Sa": "Function"
}
},
"00000000-0000-0000-0000-000000000003": {
"id": "00000000-0000-0000-0000-000000000003",
"alias": "consume",
"name": "Consume",
"description": "",
"parent": null,
"body": {
"Sa": "Function"
}
},
"00000000-0000-0000-0000-000000000004": {
"id": "00000000-0000-0000-0000-000000000004",
"alias": "packet",
"name": "Position event",
"description": "",
"parent": null,
"body": {
"Sa": {
"Shared": {
"ExchangeItem": {
"kind": "Event"
}
}
}
}
},
"00000000-0000-0000-0000-000000000005": {
"id": "00000000-0000-0000-0000-000000000005",
"alias": "coordinate",
"name": "Coordinate",
"description": "",
"parent": null,
"body": {
"Sa": {
"Shared": {
"DataType": {
"definition": "Record"
}
}
}
}
},
"00000000-0000-0000-0000-000000000006": {
"id": "00000000-0000-0000-0000-000000000006",
"alias": "latitude",
"name": "Latitude",
"description": "",
"parent": "00000000-0000-0000-0000-000000000005",
"body": {
"Sa": {
"Shared": {
"DataField": {
"type_ref": "Real",
"minimum": 1,
"maximum": 1,
"unit": "deg"
}
}
}
}
},
"00000000-0000-0000-0000-000000000007": {
"id": "00000000-0000-0000-0000-000000000007",
"alias": "origin",
"name": "Origin",
"description": "",
"parent": "00000000-0000-0000-0000-000000000004",
"body": {
"Sa": {
"Shared": {
"DataField": {
"type_ref": {
"Named": "00000000-0000-0000-0000-000000000005"
},
"minimum": 1,
"maximum": 1,
"unit": ""
}
}
}
}
},
"00000000-0000-0000-0000-000000000008": {
"id": "00000000-0000-0000-0000-000000000008",
"alias": "destination",
"name": "Destination",
"description": "",
"parent": "00000000-0000-0000-0000-000000000004",
"body": {
"Sa": {
"Shared": {
"DataField": {
"type_ref": {
"Named": "00000000-0000-0000-0000-000000000005"
},
"minimum": 1,
"maximum": 1,
"unit": ""
}
}
}
}
},
"00000000-0000-0000-0000-000000000009": {
"id": "00000000-0000-0000-0000-000000000009",
"alias": "sender_contract",
"name": "Sender contract",
"description": "",
"parent": null,
"body": {
"Sa": {
"Shared": {
"Interface": {
"items": [
"00000000-0000-0000-0000-000000000004"
]
}
}
}
}
},
"00000000-0000-0000-0000-00000000000a": {
"id": "00000000-0000-0000-0000-00000000000a",
"alias": "receiver_contract",
"name": "Receiver contract",
"description": "",
"parent": null,
"body": {
"Sa": {
"Shared": {
"Interface": {
"items": [
"00000000-0000-0000-0000-000000000004"
]
}
}
}
}
},
"00000000-0000-0000-0000-00000000000b": {
"id": "00000000-0000-0000-0000-00000000000b",
"alias": "output",
"name": "Output",
"description": "",
"parent": "00000000-0000-0000-0000-000000000002",
"body": {
"Sa": {
"Shared": {
"Port": {
"contract": {
"FunctionOutput": {
"outgoing_items": [
"00000000-0000-0000-0000-000000000004"
]
}
},
"legacy": null
}
}
}
}
},
"00000000-0000-0000-0000-00000000000c": {
"id": "00000000-0000-0000-0000-00000000000c",
"alias": "input",
"name": "Input",
"description": "",
"parent": "00000000-0000-0000-0000-000000000003",
"body": {
"Sa": {
"Shared": {
"Port": {
"contract": {
"FunctionInput": {
"incoming_items": [
"00000000-0000-0000-0000-000000000004"
]
}
},
"legacy": null
}
}
}
}
},
"00000000-0000-0000-0000-00000000000e": {
"id": "00000000-0000-0000-0000-00000000000e",
"alias": "scenario",
"name": "Position scenario",
"description": "",
"parent": null,
"body": {
"Sa": {
"Shared": {
"Scenario": {
"viewpoint": "Functional",
"context": null,
"precondition": "",
"postcondition": "",
"steps": [
"00000000-0000-0000-0000-000000000011"
]
}
}
}
}
},
"00000000-0000-0000-0000-00000000000f": {
"id": "00000000-0000-0000-0000-00000000000f",
"alias": "sender",
"name": "Sender",
"description": "",
"parent": "00000000-0000-0000-0000-00000000000e",
"body": {
"Sa": {
"Shared": {
"Lifeline": {
"represented": "00000000-0000-0000-0000-000000000002"
}
}
}
}
},
"00000000-0000-0000-0000-000000000010": {
"id": "00000000-0000-0000-0000-000000000010",
"alias": "receiver",
"name": "Receiver",
"description": "",
"parent": "00000000-0000-0000-0000-00000000000e",
"body": {
"Sa": {
"Shared": {
"Lifeline": {
"represented": "00000000-0000-0000-0000-000000000003"
}
}
}
}
},
"00000000-0000-0000-0000-000000000011": {
"id": "00000000-0000-0000-0000-000000000011",
"alias": "message",
"name": "Position",
"description": "",
"parent": "00000000-0000-0000-0000-00000000000e",
"body": {
"Sa": {
"Shared": {
"Occurrence": {
"body": {
"Exchange": {
"exchange": "00000000-0000-0000-0000-00000000000d",
"sender": "00000000-0000-0000-0000-00000000000f",
"receiver": "00000000-0000-0000-0000-000000000010",
"message_kind": "Asynchronous",
"reply_to": null,
"values": [
{
"field": "00000000-0000-0000-0000-000000000006",
"expression": "navigation.latitude"
}
]
}
},
"precondition": "",
"postcondition": ""
}
}
}
}
}
},
"relations": {
"00000000-0000-0000-0000-00000000000d": {
"id": "00000000-0000-0000-0000-00000000000d",
"alias": "position_exchange",
"name": "Position exchange",
"description": "",
"body": {
"FunctionalExchange": {
"source": "00000000-0000-0000-0000-000000000002",
"target": "00000000-0000-0000-0000-000000000003",
"source_port": "00000000-0000-0000-0000-00000000000b",
"target_port": "00000000-0000-0000-0000-00000000000c",
"items": [
"00000000-0000-0000-0000-000000000004"
]
}
}
}
},
"diagrams": {},
"artifact_links": {},
"traces": {}
}
+102 -9
View File
@@ -168,6 +168,20 @@ project mission "Harbor safety — 港 🌊" {
.find(|e| e.alias == "event_record")
.unwrap()
.id;
let associated = p
.add_element(Kind::DataType, Layer::Sa, "Associated record", None)
.unwrap();
if let Some(SharedBody::DataType { definition, .. }) =
p.elements.get_mut(&associated).unwrap().shared_mut()
{
*definition = DataTypeDefinition::Record { supertype: None };
}
p.relate(
RelationKind::DataAssociation,
"Related record",
record,
associated,
);
for (index, type_ref) in [
TypeRef::Boolean,
TypeRef::Integer,
@@ -190,18 +204,30 @@ project mission "Harbor safety — 港 🌊" {
minimum: 1,
maximum: Some(5),
unit: "unit".into(),
direction: ParameterDirection::Unspecified,
storage: FieldStorage::Value,
default: None,
};
}
for definition in [
DataTypeDefinition::Boolean,
DataTypeDefinition::Integer,
DataTypeDefinition::String,
DataTypeDefinition::Integer {
minimum: None,
maximum: None,
},
DataTypeDefinition::String {
minimum_length: 0,
maximum_length: None,
},
] {
let id = p
.add_element(Kind::DataType, Layer::Sa, "Primitive type", None)
.unwrap();
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() =
SharedBody::DataType { definition };
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
definition,
default: None,
unit: String::new(),
};
}
for kind in [
ExchangeItemKind::Material,
@@ -216,7 +242,11 @@ project mission "Harbor safety — 港 🌊" {
None,
)
.unwrap();
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem { kind };
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem {
kind,
mechanism: ExchangeMechanism::Unspecified,
supertype: None,
};
}
let scenario = p
.elements
@@ -461,7 +491,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 4; project mission \"x\" {}", "Unsupported schema"),
("arcadia 5; project mission \"x\" {}", "Unsupported schema"),
(
"arcadia 2; project mission \"x\" { oa_activity a \"A\" { nonsense 1; } }",
"Unknown field",
@@ -765,9 +795,27 @@ 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"], 3);
assert_eq!(retained["schema_version"], 4);
retained["schema_version"] = 2.into();
let allocations: Vec<_> = from_json
.relations
.values()
.filter_map(|relation| {
if let RelationBody::InterfaceItemAllocation {
source,
target,
order,
..
} = relation.body
{
Some((source, target, order))
} else {
None
}
})
.collect();
for element in retained["elements"].as_object_mut().unwrap().values_mut() {
let element_id: Id = element["id"].as_str().unwrap().parse().unwrap();
for layer in ["Oa", "Sa"] {
let Some(shared) = element["body"]
.get_mut(layer)
@@ -779,6 +827,46 @@ fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensi
assert_eq!(port["contract"], "Legacy");
*port = port["legacy"].clone();
}
if let Some(item) = shared
.get_mut("ExchangeItem")
.and_then(serde_json::Value::as_object_mut)
{
if item["mechanism"] == "Event" {
item.insert("kind".into(), "Event".into());
}
item.remove("mechanism");
item.remove("supertype");
}
if let Some(kind) = shared
.get_mut("DataType")
.and_then(serde_json::Value::as_object_mut)
{
kind.remove("default");
kind.remove("unit");
let definition = &kind["definition"];
if let Some(name) = ["Integer", "Real", "String", "Record"]
.into_iter()
.find(|name| definition.get(name).is_some())
{
kind.insert("definition".into(), name.into());
}
}
if let Some(field) = shared
.get_mut("DataField")
.and_then(serde_json::Value::as_object_mut)
{
for key in ["direction", "storage", "default"] {
field.remove(key);
}
}
if let Some(interface) = shared.get_mut("Interface") {
let mut entries: Vec<_> = allocations
.iter()
.filter(|(source, _, _)| *source == element_id)
.collect();
entries.sort_by_key(|(_, _, order)| *order);
*interface = serde_json::json!({"items":entries.into_iter().map(|(_,target,_)|target).collect::<Vec<_>>()});
}
if let Some(scenario) = shared
.get_mut("Scenario")
.and_then(serde_json::Value::as_object_mut)
@@ -797,6 +885,10 @@ fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensi
}
}
}
retained["relations"]
.as_object_mut()
.unwrap()
.retain(|_, relation| relation["body"].get("InterfaceItemAllocation").is_none());
for relation in retained["relations"].as_object_mut().unwrap().values_mut() {
if let Some(boundary) = relation["body"].get_mut("BoundaryExchange") {
assert_eq!(boundary["oriented"], true);
@@ -826,7 +918,7 @@ fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensi
);
assert_eq!(
from_json.relations.len(),
original["relations"].as_object().unwrap().len()
original["relations"].as_object().unwrap().len() + allocations.len()
);
assert_eq!(
from_json.diagrams.len(),
@@ -899,7 +991,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, 3);
assert_eq!(from_text.schema_version, 4);
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() {
@@ -1100,6 +1192,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;")
{
read(&code);
examples += 1;
+12 -5
View File
@@ -39,7 +39,7 @@ 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, 3));
assert_eq!((report.from_schema, report.to_schema), (2, 4));
assert_eq!(report.preserved_ports.len(), 4);
assert!(report.generated_endpoints.is_empty());
assert_eq!(report.orientation_defaults.len(), 1);
@@ -56,6 +56,7 @@ fn frozen_original_v2_reader_preserves_every_original_record() {
+ old["traces"].as_object().unwrap().len()
+ old["artifact_links"].as_object().unwrap().len()
+ 1
+ report.generated_interface_allocations.len()
);
let mut current = serde_json::to_value(&decoded.project).unwrap();
for diagram in current["diagrams"].as_object_mut().unwrap().values_mut() {
@@ -143,7 +144,7 @@ fn missing_v2_endpoints_are_deterministic_reported_and_independently_typed() {
}
}
let canonical = language::format(&first.project);
assert!(canonical.starts_with("arcadia 3;"));
assert!(canonical.starts_with("arcadia 4;"));
let reopened = persistence::decode_with_report(canonical.as_bytes()).unwrap();
assert!(reopened.migration.is_none());
assert_eq!(reopened.project, first.project);
@@ -223,7 +224,13 @@ fn migration_admission_counts_generated_objects_records_and_references() {
_ => 0,
})
})
.sum::<usize>();
.sum::<usize>()
+ decode(&old)
.migration
.unwrap()
.generated_interface_allocations
.len()
* 2;
for limits in [
ParseLimits {
max_graph_objects: graph + 1,
@@ -244,7 +251,7 @@ fn migration_admission_counts_generated_objects_records_and_references() {
));
}
let accepted = ParseLimits {
max_graph_objects: graph + 2,
max_graph_objects: migrated.elements.len() + migrated.relations.len(),
max_records: records,
max_step_references: reference_count,
..ParseLimits::UNLIMITED
@@ -267,7 +274,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, 3);
assert_eq!(result.project.schema_version, 4);
assert_eq!(result.migration.unwrap().from_schema, 1);
assert_eq!(persistence::decode(v1).unwrap(), result.project);
}
+6 -5
View File
@@ -1,6 +1,6 @@
use oa_core::{language, persistence, *};
const SOURCE: &str = r#"arcadia 3;
const SOURCE: &str = r#"arcadia 4;
project typed_ports "Typed ports" {
sa_system producer "Producer" {}
sa_actor consumer "Consumer" {}
@@ -9,7 +9,8 @@ project typed_ports "Typed ports" {
allocation sends "Produces" { source @producer; target @send; }
allocation receives "Consumes" { source @consumer; target @receive; }
sa_exchange_item data "Data" {}
sa_interface service "Service" { items [@data]; }
sa_interface service "Service" {}
interface_item_allocation service_data "Service data" { source @service; target @data; }
sa_port output "Output" { parent @send; contract { FunctionOutput { outgoing_items [@data]; }; }; }
sa_port input "Input" { parent @receive; contract { FunctionInput { incoming_items [@data]; }; }; }
sa_port provider "Provider port" { parent @producer; contract { Component { kind Standard; orientation Unset; }; }; }
@@ -26,7 +27,7 @@ project typed_ports "Typed ports" {
"#;
#[test]
fn source3_covers_typed_contracts_roles_allocations_and_nested_legacy_references() {
fn source4_covers_typed_contracts_roles_allocations_and_nested_legacy_references() {
let decoded = language::parse_with_report(SOURCE).unwrap();
assert!(decoded.migration.is_none());
let canonical = language::format(&decoded.project);
@@ -69,7 +70,7 @@ fn source3_covers_typed_contracts_roles_allocations_and_nested_legacy_references
}
#[test]
fn source3_requires_explicit_port_contract_and_fe_endpoints_without_generation() {
fn source4_requires_explicit_port_contract_and_fe_endpoints_without_generation() {
for (remove, expected) in [
("source_port @output; ", "source_port"),
("target_port @input; ", "target_port"),
@@ -90,7 +91,7 @@ fn source3_requires_explicit_port_contract_and_fe_endpoints_without_generation()
}
#[test]
fn source3_rejects_mixed_component_kinds_and_nested_reference_typos() {
fn source4_rejects_mixed_component_kinds_and_nested_reference_typos() {
let mixed = SOURCE.replace(
"parent @consumer; contract { Component { kind Standard; orientation Unset;",
"parent @consumer; contract { Component { kind Flow; orientation Unset;",
+228
View File
@@ -0,0 +1,228 @@
use oa_core::{language, persistence, *};
use serde_json::{Value, json};
const SOURCE3: &str = include_str!("fixtures/v3-data-contract.arcadia");
fn id(n: u128) -> Id {
Id::from_u128(n)
}
// Frozen JSON uses the same authored schema3 contract as the source fixture;
// this helper never serializes current defaults under version3.
fn original3() -> Value {
serde_json::from_str(include_str!("fixtures/v3-data-contract.json")).unwrap()
}
#[test]
fn genuine_v3_preserves_typed_ports_and_allocates_deterministic_relationships() {
let old = original3();
let decoded = persistence::decode_with_report(&serde_json::to_vec(&old).unwrap()).unwrap();
let source = language::parse_with_report(SOURCE3).unwrap();
assert_eq!(decoded.project, source.project);
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, 4));
assert_eq!(project.identities().len(), 19);
assert!(report.preserved_ports.is_empty());
assert!(report.generated_endpoints.is_empty());
assert_eq!(report.generated_interface_allocations.len(), 2);
for allocation in &report.generated_interface_allocations {
assert_eq!(
allocation.id,
Id::new_v5(
&id(1),
format!(
"v2-interface-item:{}:{}",
allocation.source, allocation.target
)
.as_bytes()
)
);
assert_eq!(allocation.target, id(4));
assert_eq!(allocation.order, 0);
assert!(matches!(
project.relations[&allocation.id].body,
RelationBody::InterfaceItemAllocation {
transmission: TransmissionProtocol::Unspecified,
acquisition: AcquisitionProtocol::Unspecified,
..
}
));
}
let now = serde_json::to_value(project).unwrap();
for port in [id(11), id(12)] {
assert_eq!(
now["elements"][port.to_string()],
old["elements"][port.to_string()]
);
}
assert!(matches!(
project.elements[&id(4)].shared(),
Some(SharedBody::ExchangeItem {
kind: ExchangeItemKind::Information,
mechanism: ExchangeMechanism::Event,
supertype: None
})
));
assert_eq!(report.unresolved_message_values.len(), 1);
assert!(
validation::validate(project)
.iter()
.any(|issue| issue.code == "message_value_ambiguous_path")
);
let canonical = language::format(project);
assert!(canonical.starts_with("arcadia 4;"));
let reopened = language::parse_with_report(&canonical).unwrap();
assert_eq!(&reopened.project, project);
assert!(reopened.migration.is_none());
assert_eq!(canonical, language::format(&reopened.project));
}
#[test]
fn v3_changed_payloads_reject_schema4_fields_and_new_constructor_names() {
let mut old = original3();
old["elements"][id(4).to_string()]["body"]["Sa"]["Shared"]["ExchangeItem"]["mechanism"] =
json!("Flow");
assert!(persistence::decode(&serde_json::to_vec(&old).unwrap()).is_err());
let mut old = original3();
old["elements"][id(5).to_string()]["body"]["Sa"]["Shared"]["DataType"]["definition"] =
json!({"Record":{"supertype":null}});
assert!(persistence::decode(&serde_json::to_vec(&old).unwrap()).is_err());
let mut old = original3();
old["elements"][id(17).to_string()]["body"]["Sa"]["Shared"]["Occurrence"]["body"]["Exchange"]
["values"][0]["path"] = json!([]);
assert!(persistence::decode(&serde_json::to_vec(&old).unwrap()).is_err());
let source = SOURCE3.replace("kind Event;", "kind Information; mechanism Flow;");
assert!(language::parse(&source).is_err());
let source = SOURCE3.replace(
"functional_exchange position_exchange",
"interface_item_allocation position_exchange",
);
assert!(language::parse(&source).is_err());
}
#[test]
fn deterministic_allocation_id_collision_is_located_without_overwriting_any_object() {
let mut old = original3();
let collision = Id::new_v5(
&id(1),
format!("v2-interface-item:{}:{}", id(9), id(4)).as_bytes(),
);
old["artifact_links"][collision.to_string()] = json!({"id":collision,"alias":"retained_artifact","name":"Retain","description":"","owner":id(1),"relation":"References","uri":"docs/contract.md","revision":null,"fragment":null});
let error = persistence::decode(&serde_json::to_vec(&old).unwrap()).unwrap_err();
match error {
persistence::FileError::Model { issues, .. } => {
assert_eq!(issues[0].code, "identity_collision");
assert_eq!(issues[0].element, Some(id(9)));
assert_eq!(issues[0].field.as_deref(), Some("items"));
}
other => panic!("unexpected error {other}"),
}
}
#[test]
fn unique_legacy_nested_value_is_qualified_and_physical_nature_is_not_guessed() {
let source = SOURCE3.replace("kind Event;", "kind Material;").replace(
"type_ref { Named @coordinate; }; }\n }",
"type_ref String; }\n }",
);
let decoded = language::parse_with_report(&source).unwrap();
assert!(matches!(
decoded.project.elements[&id(4)].shared(),
Some(SharedBody::ExchangeItem {
kind: ExchangeItemKind::Material,
mechanism: ExchangeMechanism::Unspecified,
..
})
));
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { values, .. },
..
}) = decoded.project.elements[&id(17)].shared()
else {
panic!()
};
assert_eq!(values[0].path, vec![id(7)]);
assert_eq!(values[0].expression, "navigation.latitude");
}
#[test]
fn v1_and_frozen_v2_inputs_reach_four_through_the_shared_data_upgrade() {
let v2 =
persistence::decode_with_report(include_bytes!("fixtures/v2-coastal-rescue.json")).unwrap();
let text =
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, 4);
let v1 = language::parse_with_report(
"arcadia 1; project \"Old\" id \"00000000-0000-0000-0000-000000000001\" {}",
)
.unwrap();
assert_eq!(v1.project.schema_version, 4);
assert_eq!(v1.migration.unwrap().from_schema, 1);
}
#[test]
fn allocation_alias_collisions_are_stable_and_duplicate_legacy_payload_fields_reject() {
let mut old = original3();
let first = persistence::decode_with_report(&serde_json::to_vec(&old).unwrap()).unwrap();
let alias = first.migration.unwrap().generated_interface_allocations[0]
.alias
.clone();
old["artifact_links"][id(100).to_string()] = json!({"id":id(100),"alias":alias,"name":"Existing alias","description":"","owner":id(1),"relation":"References","uri":"docs/contract.md","revision":null,"fragment":null});
let bytes = serde_json::to_vec(&old).unwrap();
let migrated = persistence::decode_with_report(&bytes).unwrap();
let generated = &migrated
.migration
.as_ref()
.unwrap()
.generated_interface_allocations[0];
assert_ne!(generated.alias, alias);
assert!(generated.alias.starts_with(&alias));
assert_eq!(
migrated.project,
persistence::decode_with_report(&bytes).unwrap().project
);
let duplicate = include_str!("fixtures/v3-data-contract.json").replace(
"\"kind\": \"Event\"",
"\"kind\": \"Event\", \"kind\": \"Material\"",
);
assert!(
persistence::decode(duplicate.as_bytes())
.unwrap_err()
.to_string()
.contains("Duplicate field 'kind'")
);
}
#[test]
fn recursive_legacy_record_paths_remain_ambiguous_instead_of_becoming_one_argument() {
let mut old = original3();
old["elements"][id(8).to_string()]["body"]["Sa"]["Shared"]["DataField"]["type_ref"] =
json!("String");
old["elements"][id(100).to_string()] = json!({"id":id(100),"alias":"next_coordinate","name":"Next coordinate","description":"","parent":id(5),"body":{"Sa":{"Shared":{"DataField":{"type_ref":{"Named":id(5)},"minimum":0,"maximum":1,"unit":""}}}}});
let decoded = persistence::decode_with_report(&serde_json::to_vec(&old).unwrap()).unwrap();
assert_eq!(
decoded
.migration
.as_ref()
.unwrap()
.unresolved_message_values
.len(),
1
);
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { values, .. },
..
}) = decoded.project.elements[&id(17)].shared()
else {
panic!()
};
assert!(values[0].path.is_empty());
assert!(
validation::validate(&decoded.project)
.iter()
.any(|issue| issue.code == "message_value_ambiguous_path")
);
}
+215
View File
@@ -0,0 +1,215 @@
use oa_core::{language, source_project::*, *};
use std::collections::BTreeMap;
fn id(n: u128) -> Id {
Id::from_u128(n)
}
#[test]
fn authored_v3_bundle_upgrade_preserves_includes_nested_ownership_comments_and_identity() {
let original = include_str!("fixtures/v3-data-contract.arcadia");
let header = original.find(" sa_function").unwrap();
let close = original.rfind('}').unwrap();
let entry = format!(
"{} // Contracts stay in their original module.\n include \"contracts.arcadia\";\n}}\n",
&original[..header]
);
let fragment = original[header..close].replace(
"items [@packet];",
"items [@ // interface contract decision\npacket];",
);
let bundle = SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([
("project.arcadia".into(), entry),
("contracts.arcadia".into(), fragment),
]),
};
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
assert_eq!(compiled.bundle.files.len(), 2);
assert!(compiled.bundle.files["project.arcadia"].contains("arcadia 4;"));
assert!(compiled.bundle.files["project.arcadia"].contains("include \"contracts.arcadia\";"));
for text in [
"// Contracts stay in their original module.",
"// Reused record parameters retain their own identities.",
"// interface contract decision",
] {
assert!(
compiled
.bundle
.files
.values()
.any(|source| source.contains(text)),
"Lost {text}"
);
}
assert_eq!(compiled.index.lexical_owners[&id(6)], id(5));
assert_eq!(compiled.index.lexical_owners[&id(7)], id(4));
assert_eq!(compiled.index.lexical_owners[&id(8)], id(4));
assert_eq!(compiled.index.lexical_owners[&id(17)], id(14));
let report = compiled.decoded.migration.as_ref().unwrap();
assert_eq!(report.from_schema, 3);
for allocation in &report.generated_interface_allocations {
assert_eq!(
compiled.index.declarations[&allocation.id].path,
"contracts.arcadia"
);
}
assert!(
compiled
.diagnostics
.iter()
.any(|d| d.diagnostic.code == "message_value_ambiguous_path")
);
let second = compile_bundle(&compiled.bundle, ProjectLimits::default()).unwrap();
assert_eq!(second.bundle, compiled.bundle);
assert_eq!(second.decoded.project, compiled.decoded.project);
assert!(second.decoded.migration.is_none());
let mut renamed = compiled.decoded.project.clone();
renamed.elements.get_mut(&id(6)).unwrap().name = "Latitude degrees".into();
let updated = update_bundle(&compiled, &renamed, ProjectLimits::default()).unwrap();
assert_eq!(updated.index.lexical_owners, compiled.index.lexical_owners);
assert!(updated.bundle.files["contracts.arcadia"].contains("Latitude degrees"));
assert!(
bundle.files["project.arcadia"].contains("arcadia 3;"),
"Migration does not mutate the caller's draft"
);
}
#[test]
fn schema4_typed_values_supertype_paths_and_protocols_roundtrip() {
let source = r#"arcadia 4; project data_contract "Data contract" {
sa_data_type bounded "Bounded" { definition { Integer { minimum -9223372036854775808; maximum 9223372036854775807; }; }; default { Integer -9223372036854775808; }; unit "m"; }
sa_data_type label_type "Label" { definition { String { minimum_length 1; maximum_length 4; }; }; default { String "航海"; }; }
sa_data_type base "Base" { definition { Record { supertype none; }; }; }
sa_data_type derived "Derived" { definition { Record { supertype @base; }; }; }
sa_exchange_item operation "Operation" { mechanism Operation;
sa_data_field value "Value" { type_ref { Named @bounded; }; direction In; storage Value; default [{ Integer 42; }]; }
}
sa_interface contract "Contract" { supertype none; }
interface_item_allocation member "Operation member" { source @contract; target @operation; transmission Synchronous; acquisition Accept; order 3; }
data_association association "Related records" { source @base; target @derived; source_role "parent"; target_role "children"; source_minimum 0; source_maximum 1; target_minimum 0; target_maximum none; source_navigable true; target_navigable true; }
}"#;
let parsed = language::parse(source).unwrap();
let formatted = language::format(&parsed);
assert!(formatted.contains("supertype @base"));
assert!(formatted.contains("transmission Synchronous"));
assert_eq!(language::parse(&formatted).unwrap(), parsed);
let compiled = compile_bundle(&format_bundle(&parsed), ProjectLimits::default()).unwrap();
assert_eq!(compiled.decoded.project, parsed);
assert!(compiled.decoded.migration.is_none());
}
#[test]
fn omitted_ids_are_stabilized_before_allocation_identity_generation() {
let bundle=SourceBundle{entry:"project.arcadia".into(),files:BTreeMap::from([("project.arcadia".into(),"arcadia 3; project old_model \"Old\" { sa_exchange_item event_item \"Event\" { kind Event; } sa_interface contract \"Contract\" { items [@event_item]; } }".into())])};
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
let report = compiled.decoded.migration.as_ref().unwrap();
let allocation = &report.generated_interface_allocations[0];
assert_eq!(
allocation.id,
Id::new_v5(
&compiled.decoded.project.id,
format!(
"v2-interface-item:{}:{}",
allocation.source, allocation.target
)
.as_bytes()
)
);
let reopened = compile_bundle(&compiled.bundle, ProjectLimits::default()).unwrap();
assert_eq!(reopened.decoded.project, compiled.decoded.project);
assert_eq!(reopened.bundle, compiled.bundle);
}
#[test]
fn inferring_a_legacy_path_keeps_rationale_inside_its_argument() {
let source = include_str!("fixtures/v3-data-contract.arcadia")
.replace(
"type_ref { Named @coordinate; }; }\n }",
"type_ref String; }\n }",
)
.replace(
"field @latitude; expression",
"field @latitude; // original argument rationale\nexpression",
);
let bundle = SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([("project.arcadia".into(), source)]),
};
let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
let source = &compiled.bundle.files["project.arcadia"];
let argument = &source[source.find("values [{").unwrap()..];
let argument = &argument[..argument.find('}').unwrap()];
assert!(
argument.contains("// original argument rationale"),
"{source}"
);
assert!(argument.contains("path [@origin]"), "{source}");
assert_eq!(compiled.index.lexical_owners[&id(17)], id(14));
}
#[test]
fn qualified_value_reordering_and_path_reidentification_keep_each_arguments_comments() {
let mut project = language::parse(include_str!("fixtures/v3-data-contract.arcadia")).unwrap();
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { values, .. },
..
}) = project.elements.get_mut(&id(17)).unwrap().shared_mut()
else {
panic!()
};
*values = vec![
MessageValue {
path: vec![id(7)],
field: id(6),
expression: "origin value".into(),
},
MessageValue {
path: vec![id(8)],
field: id(6),
expression: "destination value".into(),
},
];
let mut bundle = format_bundle(&project);
for source in bundle.files.values_mut() {
*source = source
.replace(
"expression \"origin value\";",
"expression \"origin value\"; // origin argument rationale",
)
.replace(
"expression \"destination value\";",
"expression \"destination value\"; // destination argument rationale",
);
}
let current = compile_bundle(&bundle, ProjectLimits::default()).unwrap();
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { values, .. },
..
}) = project.elements.get_mut(&id(17)).unwrap().shared_mut()
else {
panic!()
};
values.reverse();
let renamed = Id::from_u128(100);
let mut identities: BTreeMap<_, _> = project
.identities()
.into_iter()
.map(|(id, _)| (id, id))
.collect();
identities.insert(id(7), renamed);
reidentify(&mut project, id(7), renamed).unwrap();
project.elements.get_mut(&renamed).unwrap().alias = "revised_origin".into();
let result =
update_bundle_with_identity_map(&current, &project, &identities, ProjectLimits::default())
.unwrap();
let source = &result.bundle.files[&result.index.declarations[&id(17)].path];
let destination = source.find("// destination argument rationale").unwrap();
let origin = source.find("// origin argument rationale").unwrap();
assert!(
destination < origin,
"Distinct uses of the same terminal field must reorder as complete argument annotations: {source}"
);
assert!(source[origin..].contains("path [@revised_origin]"));
assert_eq!(result.decoded.project, project);
}
+6 -6
View File
@@ -5,7 +5,7 @@ fn setup() -> tempfile::TempDir {
let directory = tempfile::tempdir().unwrap();
fs::write(
directory.path().join("project.arcadia"),
"arcadia 3; project p \"P\" { include \"parts/a.arcadia\"; }\n",
"arcadia 4; project p \"P\" { include \"parts/a.arcadia\"; }\n",
)
.unwrap();
fs::create_dir(directory.path().join("parts")).unwrap();
@@ -59,7 +59,7 @@ fn loading_keeps_original_no_uuid_bytes_and_saving_stabilizes_exactly_once() {
fn invalid_and_missing_includes_keep_editable_text_and_located_diagnostics() {
let directory = setup();
let entry = directory.path().join("project.arcadia");
fs::write(&entry, "arcadia 3; project p \"P\" {\n include \"missing.arcadia\";\n include \"parts/a.arcadia\";\n}\n").unwrap();
fs::write(&entry, "arcadia 4; project p \"P\" {\n include \"missing.arcadia\";\n include \"parts/a.arcadia\";\n}\n").unwrap();
fs::write(
directory.path().join("parts/a.arcadia"),
"oa_activity a \"A\" { invalid",
@@ -102,7 +102,7 @@ fn discovery_never_refreshes_drafts_and_preserves_detached_buffers() {
let overlays = BTreeMap::from([
(
"project.arcadia".into(),
"arcadia 3; project p \"P\" { include \"new.arcadia\"; }".into(),
"arcadia 4; project p \"P\" { include \"new.arcadia\"; }".into(),
),
(
"parts/a.arcadia".into(),
@@ -176,7 +176,7 @@ fn new_destinations_require_explicit_missing_expectations_and_reject_collisions(
files: BTreeMap::from([
(
"project.arcadia".into(),
"arcadia 3; project p \"P\" { include \"child.arcadia\"; }".into(),
"arcadia 4; project p \"P\" { include \"child.arcadia\"; }".into(),
),
("child.arcadia".into(), "oa_activity a \"A\" {}".into()),
]),
@@ -256,7 +256,7 @@ fn physical_hard_link_aliases_are_rejected() {
.unwrap();
fs::write(
directory.path().join("project.arcadia"),
"arcadia 3; project p \"P\" { include \"parts/a.arcadia\"; include \"alias.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"parts/a.arcadia\"; include \"alias.arcadia\"; }",
)
.unwrap();
assert!(matches!(
@@ -286,7 +286,7 @@ fn symlinks_to_outside_or_inside_project_are_rejected() {
std::os::unix::fs::symlink(&destination, &link).unwrap();
fs::write(
directory.path().join("project.arcadia"),
"arcadia 3; project p \"P\" { include \"alias.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"alias.arcadia\"; }",
)
.unwrap();
let loaded = load_project(
@@ -6,10 +6,10 @@ fn source() -> CompiledSourceProject {
&SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([
("project.arcadia".into(), "arcadia 3; project p \"Identity review\" { include \"team/first.arcadia\"; include \"team/second.arcadia\"; include \"contracts.arcadia\"; }".into()),
("project.arcadia".into(), "arcadia 4; project p \"Identity review\" { include \"team/first.arcadia\"; include \"team/second.arcadia\"; include \"contracts.arcadia\"; }".into()),
("team/first.arcadia".into(), "// Rationale for first\nsa_exchange_item first \"First\" { description \"First rationale\"; }\n".into()),
("team/second.arcadia".into(), "// Rationale for second\nsa_exchange_item second \"Second\" {}\n".into()),
("contracts.arcadia".into(), "sa_interface contract \"Contract\" { items [\n @first // first item rationale\n ,\n @second // second item rationale\n ]; }\n".into()),
("contracts.arcadia".into(), "sa_event contract \"Contract\" { body { Signal { items [\n @first // first item rationale\n ,\n @second // second item rationale\n ]; }; }; }\n".into()),
]),
},
ProjectLimits::default(),
@@ -91,8 +91,9 @@ fn identity_swap_and_reorder_keep_both_declarations_and_item_comments_with_origi
reidentify(&mut candidate, second, first).unwrap();
reidentify(&mut candidate, temporary, second).unwrap();
let contract = find(&candidate, "contract");
let Some(SharedBody::Interface { items }) =
candidate.elements.get_mut(&contract).unwrap().shared_mut()
let Some(SharedBody::Event {
body: EventBody::Signal { items },
}) = candidate.elements.get_mut(&contract).unwrap().shared_mut()
else {
panic!("interface")
};
@@ -5,7 +5,7 @@ fn compile(a: &str, b: &str) -> CompiledSourceProject {
compile_bundle(&SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([
("project.arcadia".into(), "arcadia 3; project p \"Nested writeback\" { include \"a.arcadia\"; include \"b.arcadia\"; }\n".into()),
("project.arcadia".into(), "arcadia 4; project p \"Nested writeback\" { include \"a.arcadia\"; include \"b.arcadia\"; }\n".into()),
("a.arcadia".into(), a.into()), ("b.arcadia".into(), b.into()),
]),
}, ProjectLimits::default()).unwrap()
+35 -33
View File
@@ -30,7 +30,7 @@ fn id(project: &Project, alias: &str) -> Id {
#[test]
fn route_edits_keep_diagram_file_ownership_and_comments_with_edge_identity() {
let input = bundle(
"arcadia 3; project p \"Routing\" { include \"views.arcadia\"; include \"work.arcadia\"; }",
"arcadia 4; project p \"Routing\" { include \"views.arcadia\"; include \"work.arcadia\"; }",
&[
(
"work.arcadia",
@@ -95,7 +95,7 @@ diagram review "Review" {
#[test]
fn invalid_cross_file_route_references_and_geometry_locate_the_authored_view() {
let mut input = bundle(
"arcadia 3; project p \"Routing\" { include \"views.arcadia\"; include \"work.arcadia\"; }",
"arcadia 4; project p \"Routing\" { include \"views.arcadia\"; include \"work.arcadia\"; }",
&[
(
"work.arcadia",
@@ -127,7 +127,7 @@ fn invalid_cross_file_route_references_and_geometry_locate_the_authored_view() {
fn typed() -> SourceBundle {
bundle(
"arcadia 3; project test_model \"航海\" {\n include \"exchanges.arcadia\";\n include \"work/send.arcadia\";\n include \"work/receive.arcadia\";\n include \"types.arcadia\";\n}",
"arcadia 4; project test_model \"航海\" {\n include \"exchanges.arcadia\";\n include \"work/send.arcadia\";\n include \"work/receive.arcadia\";\n include \"types.arcadia\";\n}",
&[
(
"exchanges.arcadia",
@@ -194,7 +194,7 @@ fn cross_file_typed_ports_global_forward_references_and_stable_ids() {
#[test]
fn advisory_lint_locations_refer_to_raw_drafts_before_identity_stabilization() {
let input = bundle(
"arcadia 3; project p \"P\" { include \"work.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"work.arcadia\"; }",
&[(
"work.arcadia",
"sa_function first \"First\" {}\nsa_function second \"Second\" {}\n",
@@ -228,7 +228,7 @@ fn advisory_lint_locations_refer_to_raw_drafts_before_identity_stabilization() {
#[test]
fn source_errors_keep_exact_fragment_unicode_positions() {
let input = bundle(
"arcadia 3; project p \"P\" { include \"child.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"child.arcadia\"; }",
&[(
"child.arcadia",
"// Пример\nsa_function child \"航海\" { parent @missing; }",
@@ -274,7 +274,7 @@ fn duplicate_alias_and_identity_report_both_files_including_project_identity() {
),
] {
let input = bundle(
"arcadia 3; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
&[("a.arcadia", left), ("b.arcadia", right)],
);
let error = compile_bundle(&input, ProjectLimits::default())
@@ -285,7 +285,7 @@ fn duplicate_alias_and_identity_report_both_files_including_project_identity() {
assert_eq!(error.related[0].path, "a.arcadia");
}
let input = bundle(
"arcadia 3; project p \"P\" { include \"child.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"child.arcadia\"; }",
&[("child.arcadia", "oa_activity p \"Collision\" {}")],
);
let error = compile_bundle(&input, ProjectLimits::default())
@@ -297,7 +297,7 @@ fn duplicate_alias_and_identity_report_both_files_including_project_identity() {
#[test]
fn includes_are_once_only_and_cycles_missing_and_unreachable_files_are_located() {
let mut input = bundle(
"arcadia 3; project p \"P\" { include \"sub/a.arcadia\"; include \"sub/b.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"sub/a.arcadia\"; include \"sub/b.arcadia\"; }",
&[
("sub/a.arcadia", "include \"../shared.arcadia\";"),
("sub/b.arcadia", "include \"../shared.arcadia\";"),
@@ -356,7 +356,7 @@ fn portable_paths_reject_escapes_devices_urls_and_case_collisions() {
);
assert!(resolve_include("../bad.arcadia", "a.arcadia").is_err());
let input = bundle(
"arcadia 3; project p \"P\" {}",
"arcadia 4; project p \"P\" {}",
&[("A.arcadia", ""), ("a.arcadia", "")],
);
assert_eq!(
@@ -370,7 +370,7 @@ fn portable_paths_reject_escapes_devices_urls_and_case_collisions() {
#[test]
fn project_admission_is_aggregate_and_precedes_global_resolution() {
let input = bundle(
"arcadia 3; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
&[
("a.arcadia", "oa_activity one \"One\" { parent @missing; }"),
("b.arcadia", "oa_activity two \"Two\" {}"),
@@ -591,7 +591,7 @@ fn failed_update_preserves_candidate_and_rejects_stale_index() {
#[test]
fn single_string_and_legacy_apis_remain_explicit_and_filesystem_free() {
let source = "arcadia 3; project p \"P\" { include \"missing.arcadia\"; }";
let source = "arcadia 4; project p \"P\" { include \"missing.arcadia\"; }";
assert!(
language::parse(source).unwrap_err()[0]
.message
@@ -681,7 +681,7 @@ fn json_bundle_rejects_duplicate_file_keys_and_unknown_fields() {
#[test]
fn generated_id_insertion_handles_adjacent_header_tokens_and_remains_exactly_indexed() {
let input = bundle(
"arcadia 3;project p\"P\"{include \"child.arcadia\";}",
"arcadia 4;project p\"P\"{include \"child.arcadia\";}",
&[(
"child.arcadia",
"oa_activity a\"A\"{}diagram d\"D\"{}node n{diagram @d;element @a;}",
@@ -703,7 +703,7 @@ fn generated_id_insertion_handles_adjacent_header_tokens_and_remains_exactly_ind
#[test]
fn duplicate_include_edges_count_toward_admission_and_ordered_references_are_aggregate() {
let input = bundle(
"arcadia 3; project p \"P\" { include \"a.arcadia\"; include \"a.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"a.arcadia\"; include \"a.arcadia\"; }",
&[("a.arcadia", "")],
);
assert_eq!(
@@ -720,7 +720,7 @@ fn duplicate_include_edges_count_toward_admission_and_ordered_references_are_agg
"project_limit"
);
let input = bundle(
"arcadia 3; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
&[
(
"a.arcadia",
@@ -763,7 +763,7 @@ fn include_inspection_reuses_strict_entry_fragment_syntax_and_real_spans() {
&source[span.start..span.end],
"include \"../shared.arcadia\";"
);
let entry = "arcadia 3; project p \"P\" { include \"sub/work.arcadia\"; }";
let entry = "arcadia 4; project p \"P\" { include \"sub/work.arcadia\"; }";
assert_eq!(
inspect_includes("project.arcadia", entry, true, ProjectLimits::default()).unwrap()[0]
.target,
@@ -829,7 +829,7 @@ fn comments(source: &str) -> Vec<String> {
#[test]
fn scalar_header_guard_and_project_edits_preserve_exact_crlf_trivia_and_escape_spelling() {
let entry = "arcadia 3;\r\nproject p // project title\r\n \"Original project\" {\r\n description // project reason\r\n \"Before\";\r\n include \"behavior.arcadia\";\r\n}\r\n";
let entry = "arcadia 4;\r\nproject p // project title\r\n \"Original project\" {\r\n description // project reason\r\n \"Before\";\r\n include \"behavior.arcadia\";\r\n}\r\n";
let source = "oa_state_machine machine \"Machine\" {}\r\noa_region region_one \"Region\" { parent @machine; }\r\noa_state first \"First\" { parent @region_one; }\r\noa_state second \"Second\" { parent @region_one; }\r\nstate_transition route // route title\r\n \"Original transition\" {\r\n\tguard // feasibility\r\n\t \"old guard\"; // retain explanation\r\n\tdescription \"Keep\\u0020spelling\";\r\n\tsource @ // endpoint proof\r\n first; target @second;\r\n}\r\n";
let current = compile(&bundle(entry, &[("behavior.arcadia", source)]));
let mut project = current.decoded.project.clone();
@@ -864,7 +864,7 @@ fn scalar_header_guard_and_project_edits_preserve_exact_crlf_trivia_and_escape_s
#[test]
fn alias_updates_touch_only_header_and_reference_tokens_even_with_comments_after_at() {
let input = bundle(
"arcadia 3; project p \"P\" { include \"work.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"work.arcadia\"; }",
&[(
"work.arcadia",
"oa_activity root_work // root rationale\n \"Root\\u0020name\" { description \"root_work is prose\"; }\noa_activity child \"Child\" { parent @ // parent proof\n root_work; }\n",
@@ -887,7 +887,7 @@ fn alias_updates_touch_only_header_and_reference_tokens_even_with_comments_after
#[test]
fn omitted_defaults_stay_omitted_and_new_fields_do_not_reformat_surviving_fields() {
let input = bundle(
"arcadia 3; project p \"P\" { include \"work.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"work.arcadia\"; }",
&[(
"work.arcadia",
"oa_activity work \"Work\" {\r\n\t// owner rationale\r\n\tparent none ; // preserve spacing\r\n}\r\n",
@@ -913,7 +913,7 @@ fn omitted_defaults_stay_omitted_and_new_fields_do_not_reformat_surviving_fields
#[test]
fn kind_change_removes_field_syntax_but_keeps_its_comments_and_other_fields() {
let input = bundle(
"arcadia 3; project p \"P\" { include \"item.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"item.arcadia\"; }",
&[(
"item.arcadia",
"oa_exchange_item item \"Item\" {\n description \"Keep\\u0020this\" ; // description note\n kind // classification rationale\n Information ;\n parent none;\n}\n",
@@ -953,7 +953,7 @@ fn significant_words(source: &str) -> Vec<String> {
#[test]
fn constructor_tag_changes_retain_shared_nested_fields_and_removed_field_comments() {
let input = bundle(
"arcadia 3; project p \"P\" { include \"event.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"event.arcadia\"; }",
&[(
"event.arcadia",
"oa_event timer \"Timer\" { body { Time // time rationale\n { expression // expression rationale\n \"before\" ; kind After; // clock rationale\n }; }; }\n",
@@ -986,7 +986,7 @@ fn constructor_tag_changes_retain_shared_nested_fields_and_removed_field_comment
#[test]
fn scalar_record_constructor_changes_keep_comments_inside_removed_values() {
let input = bundle(
"arcadia 3; project p \"P\" { include \"ports.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"ports.arcadia\"; }",
&[(
"ports.arcadia",
"sa_function work \"Work\" {}\nsa_port input \"Input\" { parent @work; contract Legacy; // contract rationale\n legacy { direction In; // direction rationale\n interface none; // interface rationale\n }; }\n",
@@ -1017,10 +1017,10 @@ fn scalar_record_constructor_changes_keep_comments_inside_removed_values() {
#[test]
fn reference_list_reordering_carries_item_comments_and_alias_spelling_with_identity() {
let input = bundle(
"arcadia 3; project p \"P\" { include \"types.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"types.arcadia\"; }",
&[(
"types.arcadia",
"sa_exchange_item a \"A\" {} sa_exchange_item b \"B\" {}\nsa_interface service \"Service\" { items [\n // before A\n @ // A reference rationale\n a // after A\n ,\n // before B\n @b // after B\n ]; }\n",
"sa_exchange_item a \"A\" {} sa_exchange_item b \"B\" {}\nsa_event service \"Service\" { body { Signal { items [\n // before A\n @ // A reference rationale\n a // after A\n ,\n // before B\n @b // after B\n ]; }; }; }\n",
)],
);
let current = compile(&input);
@@ -1029,8 +1029,9 @@ fn reference_list_reordering_carries_item_comments_and_alias_spelling_with_ident
let b = id(&project, "b");
let service = id(&project, "service");
project.elements.get_mut(&a).unwrap().alias = "renamed_a".into();
if let Some(SharedBody::Interface { items }) =
project.elements.get_mut(&service).unwrap().shared_mut()
if let Some(SharedBody::Event {
body: EventBody::Signal { items },
}) = project.elements.get_mut(&service).unwrap().shared_mut()
{
*items = vec![b, a];
} else {
@@ -1051,10 +1052,10 @@ fn reference_list_reordering_carries_item_comments_and_alias_spelling_with_ident
#[test]
fn removed_list_items_and_declarations_keep_comments_as_enclosing_annotations() {
let input = bundle(
"arcadia 3; project p \"P\" { include \"types.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"types.arcadia\"; }",
&[(
"types.arcadia",
"sa_exchange_item a \"A\" { // declaration explanation\n } sa_exchange_item b \"B\" {}\nsa_interface service \"Service\" { items [\n // explanation for removed A\n @a,\n // explanation for retained B\n @b ]; }\n",
"sa_exchange_item a \"A\" { // declaration explanation\n } sa_exchange_item b \"B\" {}\nsa_event service \"Service\" { body { Signal { items [\n // explanation for removed A\n @a,\n // explanation for retained B\n @b ]; }; }; }\n",
)],
);
let current = compile(&input);
@@ -1063,8 +1064,9 @@ fn removed_list_items_and_declarations_keep_comments_as_enclosing_annotations()
let b = id(&project, "b");
let service = id(&project, "service");
project.elements.remove(&a);
if let Some(SharedBody::Interface { items }) =
project.elements.get_mut(&service).unwrap().shared_mut()
if let Some(SharedBody::Event {
body: EventBody::Signal { items },
}) = project.elements.get_mut(&service).unwrap().shared_mut()
{
*items = vec![b];
} else {
@@ -1102,7 +1104,7 @@ oa_occurrence message "Message" { parent @conversation; body { Exchange {
}; }; }
"#;
let current = compile(&bundle(
"arcadia 3; project p \"P\" { include \"conversation.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"conversation.arcadia\"; }",
&[("conversation.arcadia", source)],
));
let mut project = current.decoded.project.clone();
@@ -1151,7 +1153,7 @@ oa_occurrence message "Message" { parent @conversation; body { Exchange {
#[test]
fn added_list_items_empty_lists_and_literal_changes_preserve_existing_annotations() {
let input = bundle(
"arcadia 3; project p \"P\" { include \"types.arcadia\"; }",
"arcadia 4; project p \"P\" { include \"types.arcadia\"; }",
&[(
"types.arcadia",
"oa_data_type values \"Values\" { definition { Enumeration { literals [ // enumeration rationale\n ]; }; }; }\n",
@@ -1160,7 +1162,7 @@ fn added_list_items_empty_lists_and_literal_changes_preserve_existing_annotation
let empty = compile(&input);
let mut project = empty.decoded.project.clone();
let values = id(&project, "values");
if let Some(SharedBody::DataType { definition }) =
if let Some(SharedBody::DataType { definition, .. }) =
project.elements.get_mut(&values).unwrap().shared_mut()
{
*definition = DataTypeDefinition::Enumeration {
@@ -1185,7 +1187,7 @@ fn added_list_items_empty_lists_and_literal_changes_preserve_existing_annotation
.replace("\"Beta\"", "\"Beta\" // beta explanation\n"),
);
let annotated = compile(&annotated);
if let Some(SharedBody::DataType { definition }) =
if let Some(SharedBody::DataType { definition, .. }) =
project.elements.get_mut(&values).unwrap().shared_mut()
{
*definition = DataTypeDefinition::Enumeration {