use fhir_diff::{Filter, Pool, Report, Severity, Status, diff, load, report};
fn fixture() -> Report {
let root = env!("CARGO_MANIFEST_DIR");
let old = load(&format!("{root}/tests/fixtures/old")).expect("old package");
let new = load(&format!("{root}/tests/fixtures/new")).expect("new package");
diff(
&old.package.id(),
&Pool::new(&old),
&new.package.id(),
&Pool::new(&new),
&Filter::default(),
)
}
fn change<'a>(report: &'a Report, rule: &str, path: &str) -> Option<&'a fhir_diff::Change> {
report
.profiles
.iter()
.flat_map(|p| &p.changes)
.find(|c| c.rule == rule && c.path == path)
}
#[test]
fn every_kind_of_change_is_classified() {
let report = fixture();
let expect = |rule: &str, path: &str, severity: Severity| {
let found = change(&report, rule, path)
.unwrap_or_else(|| panic!("{rule} at {path} in {:#?}", report.profiles));
assert_eq!(found.severity, severity, "{rule} at {path}");
};
expect("min-increased", "Patient.identifier", Severity::Breaking);
expect("max-decreased", "Patient.name", Severity::Breaking);
expect("must-support-added", "Patient.name", Severity::Breaking);
expect("binding-stronger", "Patient.gender", Severity::Breaking);
expect("invariant-added", "Patient", Severity::Breaking);
expect("type-removed", "Patient.deceased", Severity::Breaking);
expect(
"target-narrowed",
"Patient.generalPractitioner",
Severity::Warning,
);
expect(
"target-removed",
"Patient.generalPractitioner",
Severity::Breaking,
);
assert!(change(&report, "must-support-added", "Patient.identifier").is_none());
}
#[test]
fn profiles_are_matched_by_url() {
let report = fixture();
let status = |name: &str| {
report
.profiles
.iter()
.find(|p| p.name == name)
.map(|p| p.status)
};
assert_eq!(status("ExamplePatient"), Some(Status::Changed));
assert_eq!(status("Retired"), Some(Status::Removed));
assert_eq!(status("ExamplePractitioner"), Some(Status::Added));
assert_eq!(report.worst(), Some(Severity::Breaking));
assert!(change(&report, "profile-removed", "Observation").is_some());
}
#[test]
fn reports_render() {
let report = fixture();
let markdown = report::markdown(&report, Severity::Info);
assert!(markdown.starts_with("# fhir-diff: example.pkg@1.0.0 → example.pkg@2.0.0"));
assert!(markdown.contains("| **breaking** | `min-increased` | `Patient.identifier` |"));
let text = report::text(&report, Severity::Info);
assert!(text.starts_with("fhir-diff example.pkg@1.0.0 → example.pkg@2.0.0\n"));
assert!(
text.lines().any(|l| l.contains("breaking")
&& l.contains("min-increased")
&& l.contains("Patient.identifier")),
"{text}"
);
assert!(
text.contains(" profiles: 1 added, 1 removed, 1 changed; "),
"{text}"
);
let json: serde_json::Value = serde_json::from_str(&report::json(&report)).unwrap();
assert_eq!(json["old"], "example.pkg@1.0.0");
let sarif: serde_json::Value =
serde_json::from_str(&report::sarif(&report, Severity::Breaking)).unwrap();
assert_eq!(sarif["version"], "2.1.0");
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert!(!results.is_empty());
assert!(results.iter().all(|r| r["level"] == "error"));
let located = &results[0]["locations"][0];
assert!(
located["physicalLocation"]["artifactLocation"]["uri"]
.as_str()
.unwrap()
.starts_with("package/StructureDefinition-")
);
}
#[test]
fn a_filter_limits_the_profiles() {
let root = env!("CARGO_MANIFEST_DIR");
let old = load(&format!("{root}/tests/fixtures/old")).unwrap();
let new = load(&format!("{root}/tests/fixtures/new")).unwrap();
let report = diff(
"old",
&Pool::new(&old),
"new",
&Pool::new(&new),
&Filter {
profiles: vec!["ExamplePractitioner".into()],
},
);
assert_eq!(report.profiles.len(), 1);
assert_eq!(report.worst(), Some(Severity::Info));
}
#[test]
fn a_missing_base_is_taken_from_the_other_package() {
let root = env!("CARGO_MANIFEST_DIR");
let old = load(&format!("{root}/tests/fixtures/borrow/old")).unwrap();
let new = load(&format!("{root}/tests/fixtures/borrow/new")).unwrap();
let (old_pool, new_pool) = Pool::pair(&old, &new);
let report = diff("old", &old_pool, "new", &new_pool, &Filter::default());
let derived = report
.profiles
.iter()
.find(|p| p.url.ends_with("/derived-obs"))
.expect("derived-obs");
assert!(derived.changes.is_empty(), "{:#?}", derived.changes);
assert!(
report
.notes
.iter()
.any(|note| note.contains("base-obs") && note.contains("taken from new")),
"{:?}",
report.notes
);
let report = diff(
"old",
&Pool::new(&old),
"new",
&Pool::new(&new),
&Filter::default(),
);
let derived = report
.profiles
.iter()
.find(|p| p.url.ends_with("/derived-obs"))
.expect("derived-obs");
assert!(
derived.changes.iter().any(|c| c.rule == "min-increased"),
"{:#?}",
derived.changes
);
}
#[test]
fn a_filter_pairs_a_renamed_profile_by_url() {
let root = env!("CARGO_MANIFEST_DIR");
let old = load(&format!("{root}/tests/fixtures/borrow/old")).unwrap();
let new = load(&format!("{root}/tests/fixtures/borrow/new")).unwrap();
let (old_pool, new_pool) = Pool::pair(&old, &new);
for name in ["DerivedObs", "DerivedObservation"] {
let report = diff(
"old",
&old_pool,
"new",
&new_pool,
&Filter {
profiles: vec![name.into()],
},
);
assert_eq!(report.profiles.len(), 1, "{name}: {:#?}", report.profiles);
assert_eq!(report.profiles[0].status, Status::Unchanged, "{name}");
}
}
#[test]
fn missing_bases_skip_core_terminology_and_known() {
use fhir_diff::package::{base_dependencies, missing_bases};
let root = env!("CARGO_MANIFEST_DIR");
let old = load(&format!("{root}/tests/fixtures/borrow/old")).unwrap();
let new = load(&format!("{root}/tests/fixtures/borrow/new")).unwrap();
assert_eq!(
missing_bases([&old.package]),
["http://example.org/StructureDefinition/base-obs".to_owned()].into()
);
assert!(missing_bases([&new.package]).is_empty());
let mut package = old.package.clone();
package.dependencies = [
("hl7.fhir.r4.core", "4.0.1"),
("hl7.terminology.r4", "7.0.1"),
("us.nlm.vsac", "0.11.0"),
("hl7.fhir.uv.sdc", "3.0.0"),
]
.into_iter()
.map(|(n, v)| (n.to_owned(), v.to_owned()))
.collect();
assert_eq!(
base_dependencies(&package).collect::<Vec<_>>(),
[("hl7.fhir.uv.sdc", "3.0.0")]
);
}