229 lines
8.8 KiB
Rust
229 lines
8.8 KiB
Rust
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, 5));
|
|
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 5;"));
|
|
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, 5);
|
|
let v1 = language::parse_with_report(
|
|
"arcadia 1; project \"Old\" id \"00000000-0000-0000-0000-000000000001\" {}",
|
|
)
|
|
.unwrap();
|
|
assert_eq!(v1.project.schema_version, 5);
|
|
assert_eq!(v1.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")
|
|
);
|
|
}
|