432 lines
15 KiB
Rust
432 lines
15 KiB
Rust
use oa_core::{language, persistence, semantic::*, source_project::*, *};
|
|
use std::collections::BTreeMap;
|
|
|
|
fn id(n: u128) -> Id {
|
|
Id::from_u128(0x40000000_0000_4000_8000_000000000000 + n)
|
|
}
|
|
|
|
fn reviewed<T>(oa: T, sa: T) -> Reviewed<T> {
|
|
Reviewed {
|
|
oa: Observed::Value(oa),
|
|
sa: Observed::Value(sa),
|
|
}
|
|
}
|
|
|
|
fn project(facets: Vec<FacetReview>) -> Project {
|
|
let mut project =
|
|
persistence::decode(include_bytes!("fixtures/v4-reviewed-prose.json")).unwrap();
|
|
project
|
|
.traces
|
|
.get_mut(&id(4))
|
|
.unwrap()
|
|
.baseline
|
|
.as_mut()
|
|
.unwrap()
|
|
.semantic = Some(SemanticBaseline { facets });
|
|
project
|
|
}
|
|
|
|
fn facets(project: &mut Project) -> &mut Vec<FacetReview> {
|
|
&mut project
|
|
.traces
|
|
.get_mut(&id(4))
|
|
.unwrap()
|
|
.baseline
|
|
.as_mut()
|
|
.unwrap()
|
|
.semantic
|
|
.as_mut()
|
|
.unwrap()
|
|
.facets
|
|
}
|
|
|
|
fn compile(source: String) -> CompiledSourceProject {
|
|
compile_bundle(
|
|
&SourceBundle {
|
|
entry: "project.arcadia".into(),
|
|
files: BTreeMap::from([("project.arcadia".into(), source)]),
|
|
},
|
|
ProjectLimits::default(),
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn typed_historical_facets_roundtrip_deleted_references_and_all_leaf_categories() {
|
|
let row = MessageValue {
|
|
path: vec![id(91), id(92)],
|
|
field: id(93),
|
|
expression: "x + 1".into(),
|
|
};
|
|
let model = project(vec![
|
|
FacetReview::Data(DataReview::Constructor(reviewed(
|
|
DataConstructor::Integer,
|
|
DataConstructor::Real,
|
|
))),
|
|
FacetReview::Data(DataReview::IntegerMinimum(Reviewed {
|
|
oa: Observed::Value(None),
|
|
sa: Observed::NotApplicable,
|
|
})),
|
|
FacetReview::Data(DataReview::RealMaximum(Reviewed {
|
|
oa: Observed::NotApplicable,
|
|
sa: Observed::Value(Some(1.5)),
|
|
})),
|
|
FacetReview::Field(FieldReview::Type(reviewed(
|
|
TypeRef::Named(id(90)),
|
|
TypeRef::Boolean,
|
|
))),
|
|
FacetReview::Field(FieldReview::Default(reviewed(
|
|
Some(vec![DataValue::String(id(90).to_string())]),
|
|
None,
|
|
))),
|
|
FacetReview::Member {
|
|
collection: ReferenceCollection::Steps,
|
|
keys: KeyPair {
|
|
oa: Some(id(91)),
|
|
sa: None,
|
|
},
|
|
review: reviewed(true, false),
|
|
},
|
|
FacetReview::Order {
|
|
collection: OrderedCollection::Steps,
|
|
review: reviewed(vec![id(91), id(92)], vec![]),
|
|
},
|
|
FacetReview::Annotation {
|
|
keys: KeyPair {
|
|
oa: Some(AnnotationKey::from(&row)),
|
|
sa: None,
|
|
},
|
|
review: reviewed(Some(row), None),
|
|
},
|
|
FacetReview::Binding {
|
|
keys: KeyPair {
|
|
oa: Some(id(94)),
|
|
sa: None,
|
|
},
|
|
review: reviewed(
|
|
Some(LifelineBinding {
|
|
referenced: id(94),
|
|
local: id(95),
|
|
}),
|
|
None,
|
|
),
|
|
},
|
|
FacetReview::Literal {
|
|
keys: KeyPair {
|
|
oa: Some(id(90).to_string()),
|
|
sa: None,
|
|
},
|
|
review: reviewed(true, false),
|
|
},
|
|
FacetReview::Artifact {
|
|
keys: KeyPair {
|
|
oa: Some(id(96)),
|
|
sa: None,
|
|
},
|
|
review: ArtifactReview::Relation(reviewed(
|
|
ArtifactRelation::References,
|
|
ArtifactRelation::References,
|
|
)),
|
|
},
|
|
]);
|
|
assert!(!model.contains(id(90)));
|
|
let source = language::format(&model);
|
|
assert!(source.starts_with("arcadia 5;"));
|
|
assert!(
|
|
source.contains(&format!("Named uuid \"{}\"", id(90))),
|
|
"{source}"
|
|
);
|
|
assert!(source.contains("sa NotApplicable;"));
|
|
assert!(source.contains("Value none;"));
|
|
assert!(source.contains(&format!("String \"{}\"", id(90))));
|
|
assert_eq!(language::parse(&source).unwrap(), model);
|
|
assert_eq!(compile(source).decoded.project, model);
|
|
assert_eq!(
|
|
persistence::decode(&serde_json::to_vec(&model).unwrap()).unwrap(),
|
|
model
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn historical_uuid_and_enum_tokens_are_contextual_and_text_remains_quoted() {
|
|
let model = project(vec![
|
|
FacetReview::Field(FieldReview::Type(reviewed(
|
|
TypeRef::Named(id(2)),
|
|
TypeRef::Boolean,
|
|
))),
|
|
FacetReview::Data(DataReview::Unit(reviewed(id(2).to_string(), "m".into()))),
|
|
]);
|
|
let source = language::format(&model);
|
|
let reference = format!("Named uuid \"{}\"", id(2));
|
|
for replacement in [format!("Named \"{}\"", id(2)), "Named @oa_dimension".into()] {
|
|
let invalid = source.replace(&reference, &replacement);
|
|
assert!(language::parse(&invalid).is_err(), "{invalid}");
|
|
assert!(
|
|
compile_bundle(
|
|
&SourceBundle {
|
|
entry: "p.arcadia".into(),
|
|
files: BTreeMap::from([("p.arcadia".into(), invalid)])
|
|
},
|
|
ProjectLimits::default()
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
let invalid_text = source.replace("Value \"m\";", &format!("Value uuid \"{}\";", id(2)));
|
|
assert!(language::parse(&invalid_text).is_err());
|
|
let invalid_enum = source.replace("Value Boolean;", "Value \"Boolean\";");
|
|
assert!(language::parse(&invalid_enum).is_err());
|
|
let invalid_live = source.replace("oa @oa_dimension;", &format!("oa uuid \"{}\";", id(2)));
|
|
assert!(language::parse(&invalid_live).is_err());
|
|
let invalid_prose = source.replace(
|
|
"name \"Approved OA name\";",
|
|
&format!("name uuid \"{}\";", id(2)),
|
|
);
|
|
assert!(language::parse(&invalid_prose).is_err());
|
|
// `uuid` remains an ordinary legal declaration alias after @.
|
|
assert!(language::parse(&source.replace("oa_dimension", "uuid")).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn semantic_values_remain_closed_typed_records_in_both_formats() {
|
|
let model = project(vec![FacetReview::Data(DataReview::IntegerMinimum(
|
|
reviewed(Some(0), None),
|
|
))]);
|
|
let source = language::format(&model);
|
|
for invalid in [
|
|
source.replace("IntegerMinimum", "UnknownFacet"),
|
|
source.replace("Value 0;", "Value \"0\";"),
|
|
source.replace("facets [", "unexpected true; facets ["),
|
|
] {
|
|
assert!(language::parse(&invalid).is_err(), "{invalid}");
|
|
}
|
|
let mut json = serde_json::to_value(&model).unwrap();
|
|
json["traces"][id(4).to_string()]["baseline"]["semantic"] = serde_json::json!({"facets": [{"Data": {"IntegerMinimum": {"oa": {"Value": "0"}, "sa": {"Value": null}}}}]});
|
|
assert!(persistence::decode(&serde_json::to_vec(&json).unwrap()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn facet_reordering_and_reidentification_keep_comments_with_typed_row_identity() {
|
|
let row = MessageValue {
|
|
path: vec![id(2)],
|
|
field: id(90),
|
|
expression: id(2).to_string(),
|
|
};
|
|
let model = project(vec![
|
|
FacetReview::Annotation {
|
|
keys: KeyPair {
|
|
oa: Some(AnnotationKey::from(&row)),
|
|
sa: None,
|
|
},
|
|
review: reviewed(Some(row), None),
|
|
},
|
|
FacetReview::Literal {
|
|
keys: KeyPair {
|
|
oa: Some(id(2).to_string()),
|
|
sa: None,
|
|
},
|
|
review: reviewed(true, false),
|
|
},
|
|
FacetReview::Field(FieldReview::Type(reviewed(
|
|
TypeRef::Named(id(2)),
|
|
TypeRef::Boolean,
|
|
))),
|
|
]);
|
|
let source = language::format(&model)
|
|
.replace("Annotation {", "Annotation { // annotation rationale")
|
|
.replace("Literal {", "Literal { // literal rationale")
|
|
.replace("Named uuid ", "Named uuid // historical type rationale\n");
|
|
let current = compile(source);
|
|
let mut prepared = prepare_command(
|
|
¤t.decoded.project,
|
|
&Command::Reidentify(id(2), id(20)),
|
|
)
|
|
.unwrap();
|
|
facets(&mut prepared.project).reverse();
|
|
let updated = update_bundle_with_identity_map(
|
|
¤t,
|
|
&prepared.project,
|
|
&prepared.identity_map,
|
|
ProjectLimits::default(),
|
|
)
|
|
.unwrap();
|
|
let text = &updated.bundle.files["project.arcadia"];
|
|
assert_eq!(updated.decoded.project, prepared.project);
|
|
assert!(
|
|
text.contains(&format!(
|
|
"Named uuid // historical type rationale\n\"{}\"",
|
|
id(20)
|
|
)),
|
|
"{text}"
|
|
);
|
|
let type_note = text.find("// historical type rationale").unwrap();
|
|
let literal_note = text.find("// literal rationale").unwrap();
|
|
let annotation_note = text.find("// annotation rationale").unwrap();
|
|
assert!(
|
|
type_note < literal_note && literal_note < annotation_note,
|
|
"{text}"
|
|
);
|
|
assert!(text.contains(&format!("expression \"{}\"", id(2))));
|
|
let FacetReview::Literal { keys, .. } = &facets(&mut prepared.project)[1] else {
|
|
panic!()
|
|
};
|
|
assert_eq!(
|
|
keys.oa,
|
|
Some(id(2).to_string()),
|
|
"UUID-shaped literal keys are text"
|
|
);
|
|
assert_eq!(
|
|
compile_bundle(&updated.bundle, ProjectLimits::default())
|
|
.unwrap()
|
|
.bundle,
|
|
updated.bundle
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn historical_reference_deletion_does_not_remove_the_review_or_create_a_live_alias() {
|
|
let mut model = project(vec![FacetReview::Field(FieldReview::Type(reviewed(
|
|
TypeRef::Named(id(90)),
|
|
TypeRef::Boolean,
|
|
)))]);
|
|
let mut former_type = model.elements[&id(2)].clone();
|
|
former_type.id = id(90);
|
|
former_type.alias = "former_type".into();
|
|
model.elements.insert(id(90), former_type);
|
|
let current = compile(language::format(&model));
|
|
let prepared = prepare_command(¤t.decoded.project, &Command::Delete(id(90))).unwrap();
|
|
let updated = update_bundle_with_identity_map(
|
|
¤t,
|
|
&prepared.project,
|
|
&prepared.identity_map,
|
|
ProjectLimits::default(),
|
|
)
|
|
.unwrap();
|
|
let source = &updated.bundle.files["project.arcadia"];
|
|
assert!(!updated.decoded.project.contains(id(90)));
|
|
assert!(!source.contains("@former_type"));
|
|
assert!(source.contains(&format!("Named uuid \"{}\"", id(90))));
|
|
assert_eq!(updated.decoded.project.traces, model.traces);
|
|
assert_eq!(language::parse(source).unwrap(), prepared.project);
|
|
}
|
|
|
|
#[test]
|
|
fn included_historical_review_edits_save_and_reopen_without_rewriting_other_modules() {
|
|
use oa_core::source_files::{load_project, save_project};
|
|
let model = project(vec![FacetReview::Field(FieldReview::Type(reviewed(
|
|
TypeRef::Named(id(90)),
|
|
TypeRef::Boolean,
|
|
)))]);
|
|
let source = language::format(&model);
|
|
let trace_at = source.find(" trace ").unwrap();
|
|
let end = source.rfind('}').unwrap();
|
|
let entry = format!(
|
|
"{} include \"reviews.arcadia\";\n}}\n",
|
|
&source[..trace_at]
|
|
);
|
|
let review =
|
|
source[trace_at..end].replace("Named uuid ", "Named uuid // deleted type retained\n");
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let entry_path = directory.path().join("project.arcadia");
|
|
std::fs::write(&entry_path, &entry).unwrap();
|
|
std::fs::write(directory.path().join("reviews.arcadia"), &review).unwrap();
|
|
let snapshot = load_project(&entry_path, ProjectLimits::default()).unwrap();
|
|
let compiled = compile_bundle(&snapshot.reachable_bundle(), ProjectLimits::default()).unwrap();
|
|
assert_eq!(compiled.index.declarations[&id(4)].path, "reviews.arcadia");
|
|
let mut changed = compiled.decoded.project.clone();
|
|
let FacetReview::Field(FieldReview::Type(value)) = &mut facets(&mut changed)[0] else {
|
|
panic!()
|
|
};
|
|
value.sa = Observed::Value(TypeRef::String);
|
|
let updated = update_bundle(&compiled, &changed, ProjectLimits::default()).unwrap();
|
|
assert_eq!(updated.bundle.files["project.arcadia"], entry);
|
|
let saved = save_project(&snapshot, &updated, ProjectLimits::default()).unwrap();
|
|
let reopened = load_project(&entry_path, ProjectLimits::default()).unwrap();
|
|
assert_eq!(saved.bundle, reopened.bundle);
|
|
assert_eq!(
|
|
compile_bundle(&reopened.reachable_bundle(), ProjectLimits::default())
|
|
.unwrap()
|
|
.decoded
|
|
.project,
|
|
changed
|
|
);
|
|
assert_eq!(std::fs::read_to_string(&entry_path).unwrap(), entry);
|
|
assert!(reopened.bundle.files["reviews.arcadia"].contains("uuid // deleted type retained"));
|
|
assert!(!changed.contains(id(90)));
|
|
}
|
|
|
|
#[test]
|
|
fn prepared_semantic_review_writes_back_and_preserves_the_accepted_target_difference() {
|
|
use oa_core::transitions::{TransitionDirection, TransitionMapping, TransitionRequest};
|
|
let model = project(vec![]);
|
|
let current = compile(language::format(&model).replace(
|
|
"baseline {",
|
|
"baseline { // prior prose review is independent",
|
|
));
|
|
let mut transition = TransitionRequest::new(TransitionDirection::OaToSa, vec![id(2)]);
|
|
transition.mappings.push(TransitionMapping {
|
|
source: id(2),
|
|
target: Some(id(3)),
|
|
target_kind: None,
|
|
});
|
|
let mut request = SemanticTransitionRequest::new(transition);
|
|
request.reviews.push(SemanticReviewRequest {
|
|
source: id(2),
|
|
resolutions: vec![
|
|
FacetResolution {
|
|
facet: FacetSelector::Data(DataFacet::Unit),
|
|
decision: SemanticDecision::KeepTarget,
|
|
},
|
|
FacetResolution {
|
|
facet: FacetSelector::Header(HeaderFacet::Name),
|
|
decision: SemanticDecision::TakeSource,
|
|
},
|
|
],
|
|
reset: vec![],
|
|
});
|
|
let plan = plan_semantic_transition(¤t.decoded.project, request).unwrap();
|
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
|
let prepared = prepare_semantic_transition(¤t.decoded.project, &plan).unwrap();
|
|
let updated = update_bundle_with_identity_map(
|
|
¤t,
|
|
&prepared.project,
|
|
&prepared.identity_map,
|
|
ProjectLimits::default(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
updated.decoded.project.elements[&id(3)].name,
|
|
"OA dimension now"
|
|
);
|
|
assert!(
|
|
matches!(updated.decoded.project.elements[&id(3)].shared(), Some(SharedBody::DataType { unit, .. }) if unit == "ft")
|
|
);
|
|
assert!(
|
|
updated.bundle.files["project.arcadia"].contains("// prior prose review is independent")
|
|
);
|
|
let baseline = updated.decoded.project.traces[&id(4)]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap();
|
|
assert_eq!(baseline.oa.name, "Approved OA name");
|
|
assert!(
|
|
baseline
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap()
|
|
.facets
|
|
.contains(&FacetReview::Data(DataReview::Unit(reviewed(
|
|
"m".into(),
|
|
"ft".into()
|
|
))))
|
|
);
|
|
assert_eq!(
|
|
language::parse(&language::format(&updated.decoded.project)).unwrap(),
|
|
prepared.project
|
|
);
|
|
assert_eq!(
|
|
persistence::decode(&serde_json::to_vec(&updated.decoded.project).unwrap()).unwrap(),
|
|
prepared.project
|
|
);
|
|
}
|