#![expect(
clippy::disallowed_types,
reason = "dev/verification tooling over JSON artifacts (the catalogue, results, wire \
exchanges), whose shapes belong to the artifacts and the SUT"
)]
use crate::artifacts::ArtifactSet;
use crate::exec::driver::HttpDriver;
use crate::exec::{CaseRecord, RowOutcome, run_case};
use crate::ids::{CapabilityName, CaseId, InstanceName, SmOperationRef};
use crate::ixit::Ixit;
use crate::model::assertion::assertion_refs;
use crate::model::case::{CaseCore, PartyRelationshipRequirement};
use crate::refgrammar::{IxitField, ValueRef};
use crate::transcript::{CaseTranscript, Recording};
use crate::vocab::CaseStatus;
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "snake_case", tag = "kind", content = "detail")]
pub enum Exception {
Unrealized(String),
Guarded(String),
Status(String),
}
#[derive(Debug, Default)]
pub struct RunReport {
pub records: Vec<CaseRecord>,
pub exceptions: Vec<(CaseId, Exception)>,
pub interpreter_run: usize,
pub considered: usize,
pub restapi_specs_version: Option<String>,
pub transcripts: Vec<CaseTranscript>,
}
impl RunReport {
#[must_use]
pub fn interpreter_coverage(&self) -> f64 {
if self.considered == 0 {
return 1.0;
}
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "case counts << 2^52"
)]
{
self.interpreter_run as f64 / self.considered as f64
}
}
}
pub(crate) fn fully_unrealized(set: &ArtifactSet, case: &CaseCore) -> Option<String> {
let anchor = case.sm_operation.as_ref()?;
let mut citations = Vec::new();
for step in &case.flow {
let op = if step.call.contains('.') {
SmOperationRef::parse(&step.call).ok()?
} else {
anchor.sibling(&step.call)
};
let binding = set
.bindings
.iter()
.map(|(_, b)| b)
.find(|b| b.sm_operation == op)?;
if let Some(decl) = &binding.unrealized {
citations.push(format!("{op}: {}", decl.ambiguity));
}
}
(!citations.is_empty()).then(|| citations.join("; "))
}
fn step_binding<'a>(
set: &'a ArtifactSet,
case: &CaseCore,
step: &crate::model::case::FlowStep,
) -> Option<&'a crate::model::binding::OperationBinding> {
let op = if step.call.contains('.') {
SmOperationRef::parse(&step.call).ok()?
} else {
case.sm_operation.as_ref()?.sibling(&step.call)
};
let mut bindings = set.bindings.iter().map(|(_, b)| b);
if let Some(variant) = step.variant.as_deref()
&& let Some(exact) = bindings
.clone()
.find(|b| b.sm_operation == op && b.variant.as_deref() == Some(variant))
{
return Some(exact);
}
bindings.find(|b| b.sm_operation == op && b.variant.is_none())
}
fn unmet_binding_floors(
set: &ArtifactSet,
case: &CaseCore,
versions: &crate::party::SpecVersions,
) -> Vec<String> {
let mut unmet = Vec::new();
for step in &case.flow {
let Some(binding) = step_binding(set, case, step) else {
continue;
};
let Some(applies) = &binding.applies else {
continue;
};
if applies.satisfied_by(versions) {
continue;
}
let declared: Vec<String> = applies
.entries()
.into_iter()
.map(|(component, range)| format!("{} {}", component.token(), range.raw()))
.collect();
let citation = format!("{} requires {}", binding.sm_operation, declared.join(", "));
if !unmet.contains(&citation) {
unmet.push(citation);
}
}
unmet
}
fn extension_family(set: &ArtifactSet, case: &CaseCore) -> Option<String> {
let anchor = case.sm_operation.as_ref()?;
for step in &case.flow {
let op = if step.call.contains('.') {
SmOperationRef::parse(&step.call).ok()?
} else {
anchor.sibling(&step.call)
};
if let Some(decl) = set
.bindings
.iter()
.map(|(_, b)| b)
.find(|b| b.sm_operation == op)
.and_then(|b| b.extension.as_ref())
{
return Some(format!("{}; {}", decl.family, decl.ambiguity));
}
}
None
}
fn capabilities_claiming_family(set: &ArtifactSet, family: &str) -> Vec<CapabilityName> {
let mut claiming: Vec<CapabilityName> = Vec::new();
for (_, case) in &set.cases {
if !extension_family(set, case)
.is_some_and(|marker| marker.starts_with(&format!("{family};")))
{
continue;
}
for capability in &case.capabilities {
if !claiming.contains(capability) {
claiming.push(capability.clone());
}
}
}
claiming
}
fn unservable_import(
set: &ArtifactSet,
statement: Option<&crate::party::Statement>,
case: &CaseCore,
) -> Result<Option<String>, String> {
if !matches!(
case.requires.import,
Some(crate::model::case::ImportRequirement::Received { .. })
) {
return Ok(None);
}
unservable_provisioning(
set,
statement,
&[
"I_EHR_EXTRACT_SERVICE.import_ehr_extract",
"I_EHR_EXTRACT_SERVICE.import_ehr",
],
"requires.import",
"the received version this case reads cannot exist here",
)
}
fn unservable_party_relationship(
set: &ArtifactSet,
statement: Option<&crate::party::Statement>,
case: &CaseCore,
) -> Result<Option<String>, String> {
if !matches!(
case.requires.party_relationship,
Some(PartyRelationshipRequirement::Exists { .. })
) {
return Ok(None);
}
unservable_provisioning(
set,
statement,
&["I_DEMOGRAPHIC_SERVICE.create_party_relationship"],
"requires.party_relationship",
"the relationship this case reads cannot exist here",
)
}
fn unservable_provisioning(
set: &ArtifactSet,
statement: Option<&crate::party::Statement>,
operations: &[&str],
requirement: &str,
consequence: &str,
) -> Result<Option<String>, String> {
let Some(statement) = statement else {
return Ok(None);
};
let mut parsed: Vec<SmOperationRef> = Vec::with_capacity(operations.len());
for call in operations {
parsed.push(SmOperationRef::parse(call).map_err(|e| {
format!("interpreter defect: selection-law SM operation anchor {call:?}: {e}")
})?);
}
let Some(decl) = parsed.iter().find_map(|op| {
set.bindings
.iter()
.map(|(_, b)| b)
.find(|b| b.sm_operation == *op)
.and_then(|b| b.extension.as_ref())
}) else {
return Ok(None);
};
let claiming = capabilities_claiming_family(set, &decl.family);
if claiming
.iter()
.any(|c| statement.claims.capabilities.contains(c))
{
return Ok(None);
}
Ok(Some(format!(
"{requirement} provisions over the {} extension routes ({}): the ICS claims none of \
the capabilities those routes' cases gate ({}), and no openEHR specification governs \
them, so {consequence}",
decl.family,
decl.ambiguity,
claiming
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)))
}
fn undeclared_ixit_facts(case: &CaseCore, ixit: &Ixit) -> Vec<&'static str> {
fn note(reference: &ValueRef, into: &mut Vec<IxitField>) {
if let ValueRef::Ixit(field) = reference
&& !into.contains(field)
{
into.push(*field);
}
}
let mut referenced: Vec<IxitField> = Vec::new();
for step in &case.flow {
for (_, value) in step.with_entries() {
for reference in value.refs() {
note(reference, &mut referenced);
}
}
for assertion in &step.assertions {
for reference in assertion_refs(assertion) {
note(&reference, &mut referenced);
}
}
}
for assertion in &case.postconditions {
for reference in assertion_refs(assertion) {
note(&reference, &mut referenced);
}
}
referenced
.into_iter()
.filter(|field| match field {
IxitField::SystemId => ixit.system_id.is_none(),
IxitField::DumpLocation => ixit.dump_location.is_none(),
})
.map(IxitField::token)
.collect()
}
fn undeclared_instances(case: &CaseCore, ixit: &Ixit) -> Vec<String> {
let mut missing: Vec<String> = Vec::new();
for step in &case.flow {
if let Some(name) = &step.on
&& ixit.instance(name).is_none()
&& !missing.iter().any(|m| m == name.as_str())
{
missing.push(name.as_str().to_owned());
}
}
missing
}
pub(crate) fn addressed_instances(case: &CaseCore) -> Vec<InstanceName> {
let mut named: Vec<InstanceName> = Vec::new();
let mut any_default = false;
for step in &case.flow {
match &step.on {
Some(name) if !named.contains(name) => named.push(name.clone()),
Some(_) => {}
None => any_default = true,
}
}
if any_default && let Ok(default) = InstanceName::parse("sut") {
named.insert(0, default);
}
named
}
fn unsatisfied_terminology(case: &CaseCore, ixit: &Ixit) -> Option<String> {
let required = case.requires.terminology.as_ref()?;
for name in addressed_instances(case) {
let Some(instance) = ixit.instance(&name) else {
continue;
};
let Some(lane) = ixit.terminology_of(instance) else {
return Some(format!(
"instance {name}: the ixit declares no `terminology` posture — the case needs a \
deployment wired to a terminology query server (BASE master12 §Binding \
Terminology Value-sets to Archetypes), and no released operation discloses one"
));
};
if let Some(posture) = required.posture
&& lane.posture != posture
{
return Some(format!(
"instance {name}: the case needs the `{}` unresolvable-value-set posture and this \
deployment declares `{}` (register AMB-172 — a deployment realizes exactly one)",
posture.token(),
lane.posture.token()
));
}
for namespace in &required.served {
match lane.server_for(namespace) {
Some(server) if server.is_reachable() => {}
Some(server) => {
return Some(format!(
"instance {name}: terminology namespace {namespace} is declared on server \
'{}', which the ixit declares unreachable — the case needs it answered",
server.name
));
}
None => {
return Some(format!(
"instance {name}: no declared terminology server answers for {namespace} \
— the party seeded no such namespace"
));
}
}
}
for namespace in &required.unreachable {
match lane.server_for(namespace) {
Some(server) if server.is_reachable() => {
return Some(format!(
"instance {name}: terminology namespace {namespace} is declared reachable \
on server '{}' — the case needs the terminology-server-down branch, \
which only a declared-unreachable server provides",
server.name
));
}
Some(_) => {}
None => {
return Some(format!(
"instance {name}: no declared terminology server answers for {namespace} \
— the party declares no such unreachable namespace"
));
}
}
}
if let Some(minimum) = required.distinct_servers {
let count = lane.distinct_reachable_servers(&required.served);
if count < minimum {
return Some(format!(
"instance {name}: the case needs {minimum} distinct reachable terminology \
servers across its namespaces and the ixit declares {count} (BASE master12 \
§Overview — several terminologies served at the same time)"
));
}
}
}
None
}
fn unsatisfied_spec_profile(case: &CaseCore, ixit: &Ixit) -> Option<String> {
let per_instance = case.requires.instances.as_ref();
for name in addressed_instances(case) {
let Some(instance) = ixit.instance(&name) else {
continue;
};
let required = per_instance
.and_then(|map| map.get(&name))
.and_then(|requires| requires.spec_profile)
.or(case.requires.spec_profile);
let Some(required) = required else {
continue;
};
match ixit.spec_profile_of(instance) {
None => {
return Some(format!(
"instance {name}: the ixit declares no `spec_profile` — the case's \
expectation rests on the `{}` generation set, and no released operation \
discloses which set a deployment runs (openEHR release strategy: a minor \
release is a compatible superset, so the sets differ only in accepted \
surface)",
required.token()
));
}
Some(declared) if declared != required => {
return Some(format!(
"instance {name}: the case needs the `{}` specification generation set and \
this deployment declares `{}` — one running server implements exactly one",
required.token(),
declared.token()
));
}
Some(_) => {}
}
}
None
}
fn unsatisfied_administrative(case: &CaseCore, ixit: &Ixit) -> Option<String> {
let per_instance = case.requires.instances.as_ref();
for name in addressed_instances(case) {
let Some(instance) = ixit.instance(&name) else {
continue;
};
let required = per_instance
.and_then(|map| map.get(&name))
.and_then(|requires| requires.administrative)
.or(case.requires.administrative);
let Some(required) = required else {
continue;
};
match instance.administrative {
None => {
return Some(format!(
"instance {name}: the ixit declares no `administrative` posture — the \
case's premise is a role boundary, and SM master02-overview.adoc \
§Functional Style delegates access control to the implementation, so \
nothing on the wire discloses which roles a principal holds"
));
}
Some(declared) if declared != required => {
return Some(format!(
"instance {name}: the ixit declares `administrative: {declared}` while \
the case's premise needs `{required}` — the role boundary the case \
drives does not exist on this deployment as declared"
));
}
Some(_) => {}
}
}
None
}
const SMART_PSEUDO_INTERFACE: &str = "I_ITS_REST_SMART";
fn needs_smart_lane(case: &CaseCore) -> bool {
case.flow
.iter()
.any(crate::model::case::FlowStep::declares_scopes)
|| case
.sm_operation
.as_ref()
.is_some_and(|op| op.interface() == SMART_PSEUDO_INTERFACE)
}
fn exclusive_server_first(set: &ArtifactSet) -> Vec<&CaseCore> {
let mut ordered: Vec<&CaseCore> = set.cases.iter().map(|(_, c)| c).collect();
ordered.sort_by_key(|c| {
!matches!(
c.requires.server,
Some(crate::vocab::ServerState::Exclusive)
)
});
ordered
}
fn not_applicable_record(case: &CaseCore, citation: &str) -> CaseRecord {
CaseRecord {
case: case.id.clone(),
format: None,
rows: vec![RowOutcome::NotApplicable {
citation: citation.to_owned(),
}],
rows_driven: 0,
rows_total: crate::exec::row_count(case),
advisories: Vec::new(),
}
}
fn unserved_extension(
set: &ArtifactSet,
statement: Option<&crate::party::Statement>,
case: &CaseCore,
) -> Result<Option<String>, String> {
if let Some(stmt) = statement
&& let Some(family) = extension_family(set, case)
&& !case
.capabilities
.iter()
.any(|c| stmt.claims.capabilities.contains(c))
{
return Ok(Some(format!(
"extension realization ({family}): the ICS claims none of this case's \
capabilities, and no openEHR specification governs the route — ISO/IEC 9646 \
test selection"
)));
}
let citation = match unservable_import(set, statement, case)? {
Some(citation) => Some(citation),
None => unservable_party_relationship(set, statement, case)?,
};
Ok(citation.map(|citation| format!("{citation} — ISO/IEC 9646 test selection")))
}
fn unclaimed_capabilities(
statement: Option<&crate::party::Statement>,
case: &CaseCore,
) -> Option<String> {
let statement = statement?;
if case.capabilities.is_empty()
|| case
.capabilities
.iter()
.any(|c| statement.claims.capabilities.contains(c))
{
return None;
}
Some(format!(
"the ICS claims none of the capabilities this case gates ({}) — CNF profiles \
master02-overview.adoc §Overview (a profile IS the list of capabilities a solution \
specifies); ISO/IEC 9646 test selection",
case.capabilities
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
))
}
fn selection_exception(
set: &ArtifactSet,
ixit: &Ixit,
statement: Option<&crate::party::Statement>,
case: &CaseCore,
) -> Result<Option<Exception>, String> {
if let Some(citation) = fully_unrealized(set, case) {
return Ok(Some(Exception::Unrealized(citation)));
}
if let Some(citation) = unserved_extension(set, statement, case)? {
return Ok(Some(Exception::Unrealized(citation)));
}
if let Some(citation) = unclaimed_capabilities(statement, case) {
return Ok(Some(Exception::Guarded(citation)));
}
if let Some(stmt) = statement
&& let Some(tag) = &case.option
&& !stmt.options.contains(tag)
{
return Ok(Some(Exception::Unrealized(format!(
"option {tag}: the ICS does not declare this register branch \
(statement.options) — ISO/IEC 9646 test selection"
))));
}
if let Some(stmt) = statement
&& !case.applies.satisfied_by(&stmt.spec_versions)
{
let declared: Vec<String> = case
.applies
.entries()
.into_iter()
.map(|(component, range)| format!("{} {}", component.token(), range.raw()))
.collect();
return Ok(Some(Exception::Unrealized(format!(
"case version floor unmet ({}) — the party's declared spec versions do not \
satisfy the case's applies ranges; ISO/IEC 9646 test selection",
declared.join(", ")
))));
}
if let Some(stmt) = statement {
let unmet = unmet_binding_floors(set, case, &stmt.spec_versions);
if !unmet.is_empty() {
return Ok(Some(Exception::Unrealized(format!(
"operation version floor unmet ({}) — the party's declared spec versions \
predate the release that introduced this wire; ISO/IEC 9646 test selection",
unmet.join("; ")
))));
}
}
if needs_smart_lane(case) && ixit.smart.is_none() {
return Ok(Some(Exception::Guarded(
"the ixit declares no `smart` lane — the case needs a SMART-enabled \
deployment and a minted, scope-carrying access token, neither of which any \
released operation discloses or provides; ISO/IEC 9646 test selection"
.to_owned(),
)));
}
if let Some(citation) = unsatisfied_terminology(case, ixit) {
return Ok(Some(Exception::Guarded(format!(
"{citation}; ISO/IEC 9646 test selection"
))));
}
if let Some(citation) = unsatisfied_spec_profile(case, ixit) {
return Ok(Some(Exception::Guarded(format!(
"{citation}; ISO/IEC 9646 test selection"
))));
}
if let Some(citation) = unsatisfied_administrative(case, ixit) {
return Ok(Some(Exception::Guarded(format!(
"{citation}; ISO/IEC 9646 test selection"
))));
}
let missing_instances = undeclared_instances(case, ixit);
if !missing_instances.is_empty() {
return Ok(Some(Exception::Guarded(format!(
"the ixit declares no instance {} — the case's flow addresses it with `on:` and \
this party runs no such deployment/principal; ISO/IEC 9646 test selection",
missing_instances.join(", ")
))));
}
let missing = undeclared_ixit_facts(case, ixit);
if !missing.is_empty() {
return Ok(Some(Exception::Guarded(format!(
"the ixit declares no {} — the case reads it as ${{ixit:…}} and no released \
operation discloses the value; ISO/IEC 9646 test selection",
missing.join(", ")
))));
}
if matches!(
case.requires.server,
Some(crate::vocab::ServerState::Exclusive)
) && !ixit
.environment
.as_ref()
.is_some_and(|env| env.exclusive_server)
{
return Ok(Some(Exception::Unrealized(
"requires.server: exclusive — the ixit declares a shared SUT instance \
(environment.exclusive_server: false); the global-state ground cannot \
be established"
.to_owned(),
)));
}
Ok(None)
}
#[derive(Debug, Clone, Copy)]
pub enum Progress<'a> {
Selected {
total: usize,
},
Driving {
completed: usize,
total: usize,
case: &'a CaseId,
},
}
impl Progress<'_> {
#[must_use]
pub fn render_line(&self) -> String {
match self {
Self::Selected { total } => format!("progress: 0/{total}"),
Self::Driving {
completed,
total,
case,
} => format!("progress: {completed}/{total} {case}"),
}
}
}
pub fn execute(
set: &ArtifactSet,
ixit: &Ixit,
statement: Option<&crate::party::Statement>,
recording: Recording,
progress: &mut dyn FnMut(Progress<'_>),
) -> Result<RunReport, String> {
let spec_versions = statement.map(|s| &s.spec_versions);
campaign(set, ixit, statement, progress, |_| {
Ok(HttpDriver::new(set, ixit, spec_versions)?.with_recording(recording))
})
}
pub fn replay(
set: &ArtifactSet,
ixit: &Ixit,
statement: Option<&crate::party::Statement>,
transcript: &crate::transcript::RunTranscript,
progress: &mut dyn FnMut(Progress<'_>),
) -> Result<RunReport, String> {
let spec_versions = statement.map(|s| &s.spec_versions);
campaign(set, ixit, statement, progress, |case| {
let exchanges = transcript
.cases
.iter()
.find(|recorded| recorded.case == *case)
.map_or(&[][..], |recorded| recorded.exchanges.as_slice());
HttpDriver::replaying(set, ixit, spec_versions, exchanges)
})
}
fn campaign<'a>(
set: &'a ArtifactSet,
ixit: &'a Ixit,
statement: Option<&crate::party::Statement>,
progress: &mut dyn FnMut(Progress<'_>),
mut driver_for: impl FnMut(&CaseId) -> Result<HttpDriver<'a>, String>,
) -> Result<RunReport, String> {
let mut report = RunReport::default();
let ordered = exclusive_server_first(set);
let total = ordered.len();
progress(Progress::Selected { total });
for (index, case) in ordered.into_iter().enumerate() {
report.considered += 1;
progress(Progress::Driving {
completed: index.saturating_add(1),
total,
case: &case.id,
});
if !matches!(case.status, CaseStatus::Active) {
report.exceptions.push((
case.id.clone(),
Exception::Status(format!("{:?} — never verdict-bearing", case.status)),
));
continue;
}
if let Some(exception) = selection_exception(set, ixit, statement, case)? {
let citation = match &exception {
Exception::Unrealized(c) | Exception::Guarded(c) | Exception::Status(c) => {
c.clone()
}
};
report.records.push(not_applicable_record(case, &citation));
report.exceptions.push((case.id.clone(), exception));
continue;
}
let runnable = if matches!(case.kind, crate::vocab::CaseKind::Content) {
synthesize_content_case(case)
} else {
case.clone()
};
let mut driver = driver_for(&case.id)?;
let format = runnable.formats.first().copied();
let record = run_case(&runnable, format, &mut driver)?;
report.interpreter_run += 1;
report.records.push(record);
let exchanges = driver.take_exchanges();
if !exchanges.is_empty() {
report.transcripts.push(CaseTranscript {
case: case.id.clone(),
format,
exchanges,
});
}
if let Some(version) = driver.take_observed_restapi_specs_version() {
report.restapi_specs_version.get_or_insert(version);
}
}
Ok(report)
}
#[must_use]
pub fn coverage_accounting(set: &ArtifactSet) -> RunReport {
let mut report = RunReport::default();
for case in exclusive_server_first(set) {
report.considered += 1;
if !matches!(case.status, CaseStatus::Active) {
report.exceptions.push((
case.id.clone(),
Exception::Status(format!("{:?}", case.status)),
));
continue;
}
if let Some(citation) = fully_unrealized(set, case) {
report
.exceptions
.push((case.id.clone(), Exception::Unrealized(citation)));
continue;
}
report.interpreter_run += 1;
}
report
}
fn refused_at_parse(columns: &[String], row: &[serde_json::Value]) -> bool {
columns
.iter()
.position(|column| column == "violates")
.and_then(|index| row.get(index))
.and_then(serde_json::Value::as_array)
.is_some_and(|violations| {
violations
.iter()
.filter_map(serde_json::Value::as_str)
.any(|violation| {
violation.starts_with("rm_schema:") && violation.contains("mandatory")
})
})
}
fn matrix_rows_from_decision_table(
table: &crate::model::case::DecisionTable,
) -> Vec<Vec<crate::model::case::MatrixCell>> {
let columns = &table.columns;
table
.rows
.iter()
.map(|row| {
columns
.iter()
.zip(row)
.map(|(column, cell)| {
if column == "expected" {
let kind = match cell.as_str() {
Some("accepted") => "created",
_ if refused_at_parse(columns, row) => "bad_request",
_ => "validation_failed",
};
crate::model::case::MatrixCell::Literal(serde_json::Value::String(
kind.to_owned(),
))
} else {
match cell {
serde_json::Value::Null => crate::model::case::MatrixCell::Null,
other => crate::model::case::MatrixCell::Literal(other.clone()),
}
}
})
.collect()
})
.collect()
}
#[must_use]
pub fn synthesize_content_case(case: &CaseCore) -> CaseCore {
let mut synthesized = case.clone();
let Some(table) = &case.decision_table else {
return synthesized;
};
let columns = table.columns.clone();
let rows = matrix_rows_from_decision_table(table);
synthesized.parameters = serde_json::from_value(serde_json::json!({
"iteration": "reset_per_row",
"matrix": { "columns": columns, "rows": [] }
}))
.ok();
if let Some(parameters) = &mut synthesized.parameters
&& let Some(matrix) = &mut parameters.matrix
{
matrix.rows = rows;
}
if let Some(context) = &case.constraint_context {
synthesized.requires.server = Some(crate::vocab::ServerState::Any);
synthesized.requires.templates = if context.constraint_columns.is_empty() {
vec![context.template.clone()]
} else {
Vec::new()
};
synthesized.requires.ehr = Some(crate::model::case::EhrRequirement::Exists {
commits: crate::model::case::CommitState::None,
});
}
synthesized.sm_operation = SmOperationRef::parse("I_EHR_COMPOSITION.create_composition").ok();
synthesized.flow = serde_json::from_value(serde_json::json!([
{
"step": 1,
"call": "create_composition",
"with": { "ehr_id": "${ehr_id}", "composition": "${recipe:content_instance(row)}" },
"expect": "created"
}
]))
.unwrap_or_default();
synthesized
}
#[cfg(test)]
mod tests {
#[test]
fn the_progress_line_grammar_is_stable() {
let case = CaseId::parse("I_EHR_SERVICE.create_ehr-main").unwrap();
assert_eq!(
Progress::Selected { total: 14 }.render_line(),
"progress: 0/14"
);
assert_eq!(
Progress::Driving {
completed: 3,
total: 14,
case: &case
}
.render_line(),
"progress: 3/14 I_EHR_SERVICE.create_ehr-main"
);
}
use super::*;
#[test]
fn a_rejected_row_splits_on_its_violation_class() {
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "CONT-DV_URI-validate_open", "kind": "content", "component": "CONTENT",
"rm_class": "DV_URI",
"test_purpose": "t", "description": "d", "spec_refs": [],
"decision_table": {
"columns": ["value", "expected", "violates"],
"rows": [
[null, "rejected", ["rm_schema: value is mandatory"]],
["xyz", "rejected", ["rm_schema: value is not a valid RFC 3986 URI"]],
["x", "rejected", ["constraint(pattern)"]],
["y", "rejected", ["rm_invariant(DV_URI.Value_valid)"]],
["z", "rejected", ["iso8601"]],
["ftp://ftp.is.co.za/rfc/rfc1808.txt", "accepted", []]
]
}
}))
.unwrap();
let synthesized = synthesize_content_case(&case);
let matrix = synthesized
.parameters
.as_ref()
.and_then(|p| p.matrix.as_ref())
.expect("the decision table becomes a parameters matrix");
let column = matrix
.columns
.iter()
.position(|c| c == "expected")
.expect("the reserved `expected` column survives synthesis");
let kinds: Vec<&str> = matrix
.rows
.iter()
.map(|row| match row.get(column) {
Some(crate::model::case::MatrixCell::Literal(serde_json::Value::String(s))) => {
s.as_str()
}
other => panic!("row expectation is not a literal kind: {other:?}"),
})
.collect();
assert_eq!(
kinds,
[
"bad_request",
"validation_failed",
"validation_failed",
"validation_failed",
"validation_failed",
"created",
]
);
}
#[test]
fn operation_version_floors_are_enforced_at_selection() {
let floored: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"its": "its-rest",
"applies": { "its_rest": ">=1.1.0" },
"request": { "method": "GET", "path": "/definition/template/adl1.4" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-floored", "kind": "functional", "component": "DEFINITION_ADL14",
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [{ "step": 1, "call": "list_opts", "expect": "ok" }]
}))
.unwrap();
let mut set = ArtifactSet::default();
set.bindings
.push((std::path::PathBuf::from("b.yaml"), floored));
let old = crate::party::SpecVersions {
its_rest: Some("1.0.3".to_owned()),
..crate::party::SpecVersions::default()
};
let unmet = unmet_binding_floors(&set, &case, &old);
assert_eq!(unmet.len(), 1, "{unmet:?}");
assert!(unmet[0].contains("I_DEFINITION_ADL14.list_opts"));
assert!(unmet[0].contains(">=1.1.0"));
let current = crate::party::SpecVersions {
its_rest: Some("1.1.0".to_owned()),
..crate::party::SpecVersions::default()
};
assert!(unmet_binding_floors(&set, &case, ¤t).is_empty());
let unfloored: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"its": "its-rest",
"request": { "method": "GET", "path": "/definition/template/adl1.4" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let mut plain = ArtifactSet::default();
plain
.bindings
.push((std::path::PathBuf::from("b.yaml"), unfloored));
assert!(unmet_binding_floors(&plain, &case, &old).is_empty());
}
#[test]
fn extension_realizations_are_marked_with_their_family_and_register_entry() {
let extension: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_PARTY_RELATIONSHIP.get_party_relationship",
"its": "its-rest",
"extension": {
"family": "party-relationship",
"reason": "the release surfaces no PARTY_RELATIONSHIP resource",
"source": "SM i_party_relationship.adoc vs ITS-REST demographic.openapi.yaml",
"ambiguity": "AMB-32"
},
"request": { "method": "GET", "path": "/demographic/party_relationship/{versioned_object_uid}" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let released: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"its": "its-rest",
"request": { "method": "GET", "path": "/definition/template/adl1.4" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let mut set = ArtifactSet::default();
set.bindings
.push((std::path::PathBuf::from("e.yaml"), extension));
set.bindings
.push((std::path::PathBuf::from("r.yaml"), released));
let on_extension: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-extension", "kind": "functional", "component": "DEMOGRAPHIC",
"sm_operation": "I_PARTY_RELATIONSHIP.get_party_relationship",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["PartyRelationshipOperations"],
"flow": [{ "step": 1, "call": "get_party_relationship", "expect": "ok" }]
}))
.unwrap();
let marker = extension_family(&set, &on_extension).expect("an extension marker");
assert!(marker.contains("party-relationship"), "{marker}");
assert!(
marker.contains("AMB-32"),
"the citation must stay register-linked: {marker}"
);
let on_released: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-released", "kind": "functional", "component": "DEFINITION_ADL14",
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [{ "step": 1, "call": "list_opts", "expect": "ok" }]
}))
.unwrap();
assert!(extension_family(&set, &on_released).is_none());
}
fn import_world() -> (ArtifactSet, CaseCore) {
let import: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_EHR_EXTRACT_SERVICE.import_ehr_extract",
"its": "its-rest",
"extension": {
"family": "message-extract",
"reason": "the release publishes no MESSAGE API",
"source": "SM master09 vs the released ITS-REST groups",
"ambiguity": "AMB-34"
},
"request": { "method": "POST", "path": "/message/import/{an_ehr_id}" },
"outcomes": { "updated": { "status": 204 } }
}))
.unwrap();
let released: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_EHR_COMPOSITION.get_versioned_composition",
"its": "its-rest",
"request": { "method": "GET", "path": "/ehr/{ehr_id}/versioned_composition/{versioned_object_uid}/version/{version_uid}" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let importer: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-import", "kind": "functional", "component": "MESSAGING",
"sm_operation": "I_EHR_EXTRACT_SERVICE.import_ehr_extract",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrExtract"],
"flow": [{ "step": 1, "call": "import_ehr_extract", "expect": "updated" }]
}))
.unwrap();
let mut set = ArtifactSet::default();
set.bindings
.push((std::path::PathBuf::from("i.yaml"), import));
set.bindings
.push((std::path::PathBuf::from("r.yaml"), released));
set.cases
.push((std::path::PathBuf::from("i-case.yaml"), importer));
let reader: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-read", "kind": "functional", "component": "EHR_COMPOSITION",
"sm_operation": "I_EHR_COMPOSITION.get_versioned_composition",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["Versioning"],
"requires": {
"ehr": { "commits": "none" },
"import": {
"extract": "cnf.messaging.ehr_extract.v1",
"container": "X_VERSIONED_COMPOSITION"
}
},
"flow": [{ "step": 1, "call": "get_versioned_composition", "expect": "ok" }]
}))
.unwrap();
(set, reader)
}
#[test]
fn an_import_precondition_is_scoped_to_the_party_that_serves_the_family() {
let (set, reader) = import_world();
let serving = statement(&["EhrExtract", "Versioning"]);
assert!(
unservable_import(&set, Some(&serving), &reader)
.expect("well-formed anchors")
.is_none(),
"a party claiming the family's capability drives the case"
);
let read_only = statement(&["Versioning"]);
let citation = unservable_import(&set, Some(&read_only), &reader)
.expect("well-formed anchors")
.expect("excused with a citation");
assert!(citation.contains("message-extract"), "{citation}");
assert!(
citation.contains("AMB-34"),
"the citation stays register-linked: {citation}"
);
let plain: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-plain", "kind": "functional", "component": "EHR_COMPOSITION",
"sm_operation": "I_EHR_COMPOSITION.get_versioned_composition",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["Versioning"],
"flow": [{ "step": 1, "call": "get_versioned_composition", "expect": "ok" }]
}))
.unwrap();
assert!(
unservable_import(&set, Some(&read_only), &plain)
.expect("well-formed anchors")
.is_none()
);
}
#[test]
fn an_unservable_precondition_is_excused_through_the_whole_law() {
let (set, reader) = import_world();
let exception = selection_exception(
&set,
&ixit(&serde_json::json!({})),
Some(&statement(&["Versioning"])),
&reader,
)
.expect("the law is decidable")
.expect("the unservable precondition excuses the case");
match &exception {
Exception::Unrealized(citation) => {
assert!(citation.contains("requires.import"), "{citation}");
assert!(citation.contains("AMB-34"), "{citation}");
assert!(
citation.contains("ISO/IEC 9646 test selection"),
"{citation}"
);
}
other => panic!("expected an unrealized exception, got {other:?}"),
}
assert!(
selection_exception(
&set,
&ixit(&serde_json::json!({})),
Some(&statement(&["EhrExtract", "Versioning"])),
&reader,
)
.expect("the law is decidable")
.is_none()
);
}
#[test]
fn a_party_relationship_precondition_is_scoped_at_selection() {
let create: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_DEMOGRAPHIC_SERVICE.create_party_relationship",
"its": "its-rest",
"extension": {
"family": "party-relationship",
"reason": "the release surfaces no PARTY_RELATIONSHIP resource",
"source": "SM docs/UML/classes/i_demographic_service.adoc",
"ambiguity": "AMB-32"
},
"request": { "method": "POST", "path": "/demographic/party_relationship" },
"outcomes": { "created": { "status": 201 } }
}))
.unwrap();
let released: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_PARTY.get_party",
"its": "its-rest",
"request": { "method": "GET", "path": "/demographic/party/{party_id}" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let creator: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-rel", "kind": "functional", "component": "DEMOGRAPHIC",
"sm_operation": "I_DEMOGRAPHIC_SERVICE.create_party_relationship",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["PartyRelationships"],
"flow": [{ "step": 1, "call": "create_party_relationship", "expect": "created" }]
}))
.unwrap();
let mut set = ArtifactSet::default();
set.bindings
.push((std::path::PathBuf::from("c.yaml"), create));
set.bindings
.push((std::path::PathBuf::from("r.yaml"), released));
set.cases
.push((std::path::PathBuf::from("c-case.yaml"), creator));
let reader: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-party-read", "kind": "functional", "component": "DEMOGRAPHIC",
"sm_operation": "I_PARTY.get_party",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["Demographics"],
"requires": {
"party_relationship": {
"source": "cnf.demographic.person.v1",
"target": "cnf.demographic.organisation.v1",
"relationship": "cnf.demographic.party_relationship.v1"
}
},
"flow": [{ "step": 1, "call": "get_party", "expect": "ok" }]
}))
.unwrap();
let statement = |caps: &[&str]| -> crate::party::Statement {
serde_json::from_value(serde_json::json!({
"product": { "name": "p", "version": "1", "vendor": "v", "identifier": "i" },
"schedule_release": "CNF-2.0",
"spec_versions": { "rm": "1.2.0", "its_rest": "1.1.0" },
"claims": { "capabilities": caps, "profiles": ["CORE"] },
"tech_profiles": [ { "its": "its-rest", "formats": ["canonical-json"] } ],
"options": []
}))
.unwrap()
};
let serving = statement(&["PartyRelationships", "Demographics"]);
assert!(
unservable_party_relationship(&set, Some(&serving), &reader)
.expect("well-formed anchors")
.is_none(),
"a party claiming the family's capability drives the case"
);
let read_only = statement(&["Demographics"]);
let citation = unservable_party_relationship(&set, Some(&read_only), &reader)
.expect("well-formed anchors")
.expect("excused with a citation");
assert!(citation.contains("party-relationship"), "{citation}");
assert!(
citation.contains("AMB-32"),
"the citation stays register-linked: {citation}"
);
let plain: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-plain-party", "kind": "functional", "component": "DEMOGRAPHIC",
"sm_operation": "I_PARTY.get_party",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["Demographics"],
"requires": { "party_relationship": "none" },
"flow": [{ "step": 1, "call": "get_party", "expect": "ok" }]
}))
.unwrap();
assert!(
unservable_party_relationship(&set, Some(&read_only), &plain)
.expect("well-formed anchors")
.is_none()
);
}
#[test]
fn smart_lane_need_is_declared_not_guessed() {
let plain: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-plain", "kind": "functional", "component": "SECURITY",
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [{ "step": 1, "call": "list_opts", "expect": "ok" }]
}))
.unwrap();
assert!(!needs_smart_lane(&plain));
let scoped: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-scoped", "kind": "functional", "component": "SECURITY",
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [{ "step": 1, "call": "list_opts", "scopes": [], "expect": "forbidden" }]
}))
.unwrap();
assert!(
needs_smart_lane(&scoped),
"an EMPTY scopes declaration is still a SMART-lane declaration"
);
let discovery: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-discovery", "kind": "functional", "component": "SECURITY",
"sm_operation": "I_ITS_REST_SMART.discovery",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [{ "step": 1, "call": "discovery", "expect": "ok" }]
}))
.unwrap();
assert!(needs_smart_lane(&discovery));
}
#[test]
fn undeclared_addressed_instances_are_collected() {
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-two-deployments", "kind": "functional", "component": "SECURITY",
"sm_operation": "I_DEFINITION_ADL14.list_opts",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [
{ "step": 1, "call": "list_opts", "expect": "ok" },
{ "step": 2, "call": "list_opts", "on": "sut_pgp", "expect": "ok" },
{ "step": 3, "call": "list_opts", "on": "sut_pgp", "expect": "ok" }
]
}))
.unwrap();
let without: Ixit = serde_json::from_value(serde_json::json!({
"instances": { "sut": { "base_url": "http://x", "auth": { "mode": "none" } } }
}))
.unwrap();
assert_eq!(undeclared_instances(&case, &without), vec!["sut_pgp"]);
let with: Ixit = serde_json::from_value(serde_json::json!({
"instances": {
"sut": { "base_url": "http://x", "auth": { "mode": "none" } },
"sut_pgp": { "base_url": "http://y", "auth": { "mode": "none" } }
}
}))
.unwrap();
assert!(undeclared_instances(&case, &with).is_empty());
}
#[test]
fn terminology_requirements_are_selected_against_the_declaration() {
let case = |requirement: serde_json::Value| -> CaseCore {
serde_json::from_value(serde_json::json!({
"id": "X-terminology", "kind": "functional", "component": "QUERY",
"sm_operation": "I_QUERY_SERVICE.execute_ad_hoc_query",
"test_purpose": "t", "description": "d", "spec_refs": [],
"requires": { "server": "any", "terminology": requirement },
"flow": [{ "step": 1, "call": "execute_ad_hoc_query", "expect": "ok" }]
}))
.unwrap()
};
let ixit: Ixit = serde_json::from_value(serde_json::json!({
"instances": { "sut": { "base_url": "http://x", "auth": { "mode": "none" } } },
"terminology": {
"posture": "fail_open",
"servers": [
{ "name": "sct", "namespaces": ["urn:cnf:sct"] },
{ "name": "loinc", "namespaces": ["urn:cnf:loinc"] },
{ "name": "down", "reachable": false, "namespaces": ["urn:cnf:down"] }
]
}
}))
.unwrap();
let plain: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-plain", "kind": "functional", "component": "QUERY",
"sm_operation": "I_QUERY_SERVICE.execute_ad_hoc_query",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [{ "step": 1, "call": "execute_ad_hoc_query", "expect": "ok" }]
}))
.unwrap();
assert!(unsatisfied_terminology(&plain, &ixit).is_none());
assert!(
unsatisfied_terminology(
&case(serde_json::json!({
"posture": "fail_open",
"served": ["urn:cnf:sct", "urn:cnf:loinc"],
"distinct_servers": 2
})),
&ixit
)
.is_none()
);
let undeclared: Ixit = serde_json::from_value(serde_json::json!({
"instances": { "sut": { "base_url": "http://x", "auth": { "mode": "none" } } }
}))
.unwrap();
let citation = unsatisfied_terminology(
&case(serde_json::json!({ "served": ["urn:cnf:sct"] })),
&undeclared,
)
.expect("undeclared lane is a selection outcome");
assert!(
citation.contains("declares no `terminology` posture"),
"{citation}"
);
let citation = unsatisfied_terminology(
&case(serde_json::json!({ "posture": "fail_closed" })),
&ixit,
)
.expect("posture mismatch is a selection outcome");
assert!(citation.contains("fail_closed"), "{citation}");
assert!(
unsatisfied_terminology(
&case(serde_json::json!({ "served": ["urn:cnf:absent"] })),
&ixit
)
.is_some_and(|c| c.contains("urn:cnf:absent"))
);
assert!(
unsatisfied_terminology(
&case(serde_json::json!({ "unreachable": ["urn:cnf:down"] })),
&ixit
)
.is_none()
);
assert!(
unsatisfied_terminology(
&case(serde_json::json!({ "unreachable": ["urn:cnf:sct"] })),
&ixit
)
.is_some_and(|c| c.contains("declared reachable"))
);
assert!(
unsatisfied_terminology(
&case(serde_json::json!({
"served": ["urn:cnf:sct"], "distinct_servers": 2
})),
&ixit
)
.is_some_and(|c| c.contains("2 distinct reachable"))
);
}
#[test]
fn spec_profile_requirements_are_selected_against_the_declaration() {
let case = |requires: serde_json::Value| -> CaseCore {
serde_json::from_value(serde_json::json!({
"id": "X-profile", "kind": "functional", "component": "EHR_COMPOSITION",
"sm_operation": "I_EHR_COMPOSITION.get_composition_at_version",
"test_purpose": "t", "description": "d", "spec_refs": [],
"requires": requires,
"flow": [
{ "step": 1, "call": "get_composition_at_version", "expect": "ok" },
{ "step": 2, "call": "get_composition_at_version",
"on": "sut_stable", "expect": "conflict" }
]
}))
.unwrap()
};
let ixit: Ixit = serde_json::from_value(serde_json::json!({
"instances": {
"sut": { "base_url": "http://x", "auth": { "mode": "none" } },
"sut_stable": { "base_url": "http://y", "auth": { "mode": "none" },
"spec_profile": "stable" }
},
"spec_profile": "development"
}))
.unwrap();
assert!(unsatisfied_spec_profile(&case(serde_json::json!({})), &ixit).is_none());
assert!(
unsatisfied_spec_profile(
&case(serde_json::json!({
"spec_profile": "development",
"instances": { "sut_stable": { "spec_profile": "stable" } }
})),
&ixit
)
.is_none()
);
assert!(
unsatisfied_spec_profile(
&case(serde_json::json!({
"instances": { "sut_stable": { "spec_profile": "development" } }
})),
&ixit
)
.is_some_and(|c| c.contains("sut_stable") && c.contains("exactly one"))
);
let citation = unsatisfied_spec_profile(
&case(serde_json::json!({ "spec_profile": "stable" })),
&ixit,
)
.expect("set mismatch is a selection outcome");
assert!(
citation.contains("`stable`") && citation.contains("`development`"),
"{citation}"
);
let undeclared: Ixit = serde_json::from_value(serde_json::json!({
"instances": {
"sut": { "base_url": "http://x", "auth": { "mode": "none" } },
"sut_stable": { "base_url": "http://y", "auth": { "mode": "none" } }
}
}))
.unwrap();
let citation = unsatisfied_spec_profile(
&case(serde_json::json!({ "spec_profile": "development" })),
&undeclared,
)
.expect("undeclared set is a selection outcome");
assert!(
citation.contains("declares no `spec_profile`"),
"{citation}"
);
}
#[test]
fn administrative_requirements_are_selected_against_the_declaration() {
let case = |requires: serde_json::Value| -> CaseCore {
serde_json::from_value(serde_json::json!({
"id": "X-role", "kind": "functional", "component": "ADMIN",
"sm_operation": "I_ADMIN_ARCHIVE.archive_ehrs",
"test_purpose": "t", "description": "d", "spec_refs": [],
"requires": requires,
"flow": [{ "step": 1, "call": "archive_ehrs", "expect": "forbidden" }]
}))
.unwrap()
};
let ixit = |sut: serde_json::Value| -> Ixit {
serde_json::from_value(serde_json::json!({ "instances": { "sut": sut } })).unwrap()
};
let non_admin = ixit(serde_json::json!({
"base_url": "http://x", "auth": { "mode": "none" }, "administrative": false
}));
let admin = ixit(serde_json::json!({
"base_url": "http://x", "auth": { "mode": "none" }, "administrative": true
}));
let undeclared = ixit(serde_json::json!({
"base_url": "http://x", "auth": { "mode": "none" }
}));
let needs_non_admin = serde_json::json!({
"instances": { "sut": { "administrative": false } }
});
assert!(unsatisfied_administrative(&case(serde_json::json!({})), &admin).is_none());
assert!(unsatisfied_administrative(&case(needs_non_admin.clone()), &non_admin).is_none());
let citation = unsatisfied_administrative(&case(needs_non_admin.clone()), &undeclared)
.expect("undeclared posture is a selection outcome");
assert!(
citation.contains("declares no `administrative`") && citation.contains("sut"),
"{citation}"
);
let citation = unsatisfied_administrative(&case(needs_non_admin), &admin)
.expect("an opposite posture is a selection outcome");
assert!(
citation.contains("administrative: true") && citation.contains("`false`"),
"{citation}"
);
assert!(
unsatisfied_administrative(
&case(serde_json::json!({ "administrative": false })),
&admin
)
.is_some()
);
}
#[test]
fn coverage_gate_holds_on_the_committed_catalogue() {
let crate_dir = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."));
let loaded = crate::artifacts::load_root(&crate_dir.join("artifacts")).unwrap();
assert!(loaded.errors.is_empty());
let report = coverage_accounting(&loaded.set);
let governed = report.interpreter_run
+ report
.exceptions
.iter()
.filter(|(_, e)| matches!(e, Exception::Unrealized(_)))
.count();
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "case counts << 2^52, so the coverage ratio is exact enough"
)]
let coverage = governed as f64 / report.considered as f64;
assert!(
coverage >= 0.80,
"interpreter-governed coverage {coverage:.3} below the floor; exceptions: {:#?}",
report.exceptions.len()
);
for (case, exception) in &report.exceptions {
let text = format!("{exception:?}");
assert!(!text.is_empty(), "{case}: silent exception");
}
}
fn statement(capabilities: &[&str]) -> crate::party::Statement {
serde_json::from_value(serde_json::json!({
"product": { "name": "p", "version": "1", "vendor": "v", "identifier": "i" },
"schedule_release": "CNF-2.0",
"spec_versions": { "rm": "1.2.0", "its_rest": "1.1.0" },
"claims": { "capabilities": capabilities, "profiles": ["CORE"] },
"tech_profiles": [ { "its": "its-rest", "formats": ["canonical-json"] } ],
"options": []
}))
.unwrap()
}
#[test]
fn a_case_gating_only_unclaimed_capabilities_is_selected_away() {
let signing: CaseCore = serde_json::from_value(serde_json::json!({
"id": "SIG-VERSION-ehr_status_signature", "kind": "functional", "component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["Signing"],
"flow": [{ "step": 1, "call": "get_ehr_status", "expect": "ok" }]
}))
.unwrap();
let claimed = statement(&["EhrOperations", "Signing"]);
let unclaimed = statement(&["EhrOperations"]);
let citation = unclaimed_capabilities(Some(&unclaimed), &signing)
.expect("an unclaimed capability takes the case out of scope");
assert!(citation.contains("Signing"), "{citation}");
assert!(citation.contains("ISO/IEC 9646"), "{citation}");
assert!(unclaimed_capabilities(Some(&claimed), &signing).is_none());
assert!(unclaimed_capabilities(None, &signing).is_none());
let mut partly = signing.clone();
partly.capabilities = vec![
CapabilityName::parse("Signing").unwrap(),
CapabilityName::parse("EhrOperations").unwrap(),
];
assert!(unclaimed_capabilities(Some(&unclaimed), &partly).is_none());
let mut capability_less = signing;
capability_less.capabilities.clear();
assert!(unclaimed_capabilities(Some(&unclaimed), &capability_less).is_none());
}
fn selection_world() -> (ArtifactSet, CaseCore) {
let binding: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"its": "its-rest",
"request": { "method": "GET", "path": "/ehr/{ehr_id}/ehr_status" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let mut set = ArtifactSet::default();
set.bindings
.push((std::path::PathBuf::from("b.yaml"), binding));
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_STATUS.get_ehr_status-main", "kind": "functional", "component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrStatus"],
"flow": [{ "step": 1, "call": "get_ehr_status", "expect": "ok" }]
}))
.unwrap();
(set, case)
}
fn ixit(extra: &serde_json::Value) -> Ixit {
let mut document = serde_json::json!({
"instances": {
"sut": { "base_url": "http://sut.test/openehr/v1", "auth": { "mode": "none" } }
}
});
if let (Some(target), Some(source)) = (document.as_object_mut(), extra.as_object()) {
for (key, value) in source {
target.insert(key.clone(), value.clone());
}
}
serde_json::from_value(document).unwrap()
}
#[test]
fn a_case_this_party_declares_everything_for_drives() {
let (set, case) = selection_world();
let statement = statement(&["EhrStatus"]);
assert!(
selection_exception(&set, &ixit(&serde_json::json!({})), Some(&statement), &case)
.expect("the law is decidable")
.is_none(),
"nothing excuses a fully declared case"
);
}
#[test]
fn an_undeclared_option_branch_is_excused_at_selection() {
let (set, mut case) = selection_world();
case.option = Some(crate::ids::OptionTag::parse("terminology-fail-closed").unwrap());
let statement = statement(&["EhrStatus"]);
let exception =
selection_exception(&set, &ixit(&serde_json::json!({})), Some(&statement), &case)
.expect("the law is decidable")
.expect("an undeclared option branch is excused");
match &exception {
Exception::Unrealized(citation) => {
assert!(citation.contains("terminology-fail-closed"), "{citation}");
assert!(citation.contains("statement.options"), "{citation}");
}
other => panic!("expected an unrealized exception, got {other:?}"),
}
assert!(
selection_exception(&set, &ixit(&serde_json::json!({})), None, &case)
.expect("the law is decidable")
.is_none()
);
}
#[test]
fn a_case_version_floor_the_party_predates_is_excused() {
let (set, _) = selection_world();
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_STATUS.get_ehr_status-dated", "kind": "functional", "component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrStatus"],
"applies": { "its_rest": ">=2.0.0" },
"flow": [{ "step": 1, "call": "get_ehr_status", "expect": "ok" }]
}))
.unwrap();
let statement = statement(&["EhrStatus"]);
let exception =
selection_exception(&set, &ixit(&serde_json::json!({})), Some(&statement), &case)
.expect("the law is decidable")
.expect("an unmet case floor is excused");
match &exception {
Exception::Unrealized(citation) => {
assert!(citation.contains("case version floor unmet"), "{citation}");
assert!(citation.contains(">=2.0.0"), "{citation}");
}
other => panic!("expected an unrealized exception, got {other:?}"),
}
}
#[test]
fn a_scope_carrying_case_needs_a_declared_smart_lane() {
let (set, _) = selection_world();
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_STATUS.get_ehr_status-scoped", "kind": "functional", "component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrStatus"],
"flow": [{
"step": 1, "call": "get_ehr_status", "expect": "ok",
"scopes": ["patient/EHR_STATUS.r"]
}]
}))
.unwrap();
assert!(needs_smart_lane(&case));
let statement = statement(&["EhrStatus"]);
let exception =
selection_exception(&set, &ixit(&serde_json::json!({})), Some(&statement), &case)
.expect("the law is decidable")
.expect("no SMART lane, no scoped case");
match &exception {
Exception::Guarded(citation) => {
assert!(citation.contains("no `smart` lane"), "{citation}");
assert!(citation.contains("ISO/IEC 9646"), "{citation}");
}
other => panic!("expected a guarded exception, got {other:?}"),
}
}
#[test]
fn a_step_addressing_an_undeclared_instance_is_excused_by_name() {
let (set, _) = selection_world();
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_STATUS.get_ehr_status-readonly", "kind": "functional", "component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrStatus"],
"flow": [{ "step": 1, "call": "get_ehr_status", "expect": "ok", "on": "readonly" }]
}))
.unwrap();
let topology = ixit(&serde_json::json!({}));
assert_eq!(undeclared_instances(&case, &topology), vec!["readonly"]);
let exception =
selection_exception(&set, &topology, Some(&statement(&["EhrStatus"])), &case)
.expect("the law is decidable")
.expect("an undeclared instance is excused");
match &exception {
Exception::Guarded(citation) => {
assert!(citation.contains("no instance readonly"), "{citation}");
}
other => panic!("expected a guarded exception, got {other:?}"),
}
let declared = ixit(&serde_json::json!({
"instances": {
"sut": { "base_url": "http://sut.test/openehr/v1", "auth": { "mode": "none" } },
"readonly": { "base_url": "http://sut.test/openehr/v1", "auth": { "mode": "none" } }
}
}));
assert!(undeclared_instances(&case, &declared).is_empty());
}
#[test]
fn a_case_reading_an_undeclared_ixit_fact_is_excused() {
let (set, _) = selection_world();
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_STATUS.get_ehr_status-system_id", "kind": "functional",
"component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrStatus"],
"flow": [{
"step": 1, "call": "get_ehr_status", "expect": "ok",
"assert": [
{ "assert": "field", "path": "system_id", "equals": "${ixit:system_id}" }
]
}]
}))
.unwrap();
let undeclared = ixit(&serde_json::json!({}));
assert_eq!(undeclared_ixit_facts(&case, &undeclared), vec!["system_id"]);
let exception =
selection_exception(&set, &undeclared, Some(&statement(&["EhrStatus"])), &case)
.expect("the law is decidable")
.expect("an undeclared ixit fact is excused");
match &exception {
Exception::Guarded(citation) => {
assert!(citation.contains("no system_id"), "{citation}");
assert!(citation.contains("${ixit:"), "{citation}");
}
other => panic!("expected a guarded exception, got {other:?}"),
}
let declared = ixit(&serde_json::json!({ "system_id": "sut.example.org" }));
assert!(undeclared_ixit_facts(&case, &declared).is_empty());
}
#[test]
fn an_exclusive_server_ground_is_not_established_on_a_shared_instance() {
let (set, _) = selection_world();
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_STATUS.get_ehr_status-empty", "kind": "functional", "component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrStatus"],
"requires": { "server": "exclusive" },
"flow": [{ "step": 1, "call": "get_ehr_status", "expect": "ok" }]
}))
.unwrap();
let environment = serde_json::json!({
"environment": {
"exclusive_server": false, "hardware_class": "laptop", "cores": 8,
"memory_gb": 16, "storage_class": "nvme ssd", "topology": "single node"
}
});
let exception = selection_exception(
&set,
&ixit(&environment),
Some(&statement(&["EhrStatus"])),
&case,
)
.expect("the law is decidable")
.expect("a shared instance cannot establish the ground");
match &exception {
Exception::Unrealized(citation) => {
assert!(
citation.contains("requires.server: exclusive"),
"{citation}"
);
}
other => panic!("expected an unrealized exception, got {other:?}"),
}
let mut exclusive = environment;
exclusive["environment"]["exclusive_server"] = serde_json::json!(true);
assert!(
selection_exception(
&set,
&ixit(&exclusive),
Some(&statement(&["EhrStatus"])),
&case
)
.expect("the law is decidable")
.is_none()
);
}
#[test]
fn a_case_resting_on_a_generation_set_needs_it_declared() {
let (set, _) = selection_world();
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_STATUS.get_ehr_status-stable", "kind": "functional", "component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrStatus"],
"requires": { "spec_profile": "stable" },
"flow": [{ "step": 1, "call": "get_ehr_status", "expect": "ok" }]
}))
.unwrap();
let undeclared = ixit(&serde_json::json!({}));
let citation = unsatisfied_spec_profile(&case, &undeclared)
.expect("an undeclared generation set excuses the case");
assert!(citation.contains("no `spec_profile`"), "{citation}");
let other = ixit(&serde_json::json!({ "spec_profile": "development" }));
let citation = unsatisfied_spec_profile(&case, &other)
.expect("the other generation set excuses the case");
assert!(citation.contains("`stable`"), "{citation}");
assert!(citation.contains("`development`"), "{citation}");
let declared = ixit(&serde_json::json!({ "spec_profile": "stable" }));
assert!(unsatisfied_spec_profile(&case, &declared).is_none());
let exception = selection_exception(&set, &other, Some(&statement(&["EhrStatus"])), &case)
.expect("the law is decidable")
.expect("the wrong generation set is excused");
assert!(matches!(exception, Exception::Guarded(_)), "{exception:?}");
}
#[test]
fn a_terminology_backed_case_needs_a_declared_lane() {
let (set, _) = selection_world();
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_STATUS.get_ehr_status-terminology", "kind": "functional",
"component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrStatus"],
"requires": { "terminology": { "served": ["SNOMED-CT"] } },
"flow": [{ "step": 1, "call": "get_ehr_status", "expect": "ok" }]
}))
.unwrap();
let bare = ixit(&serde_json::json!({}));
let citation = unsatisfied_terminology(&case, &bare)
.expect("an undeclared terminology lane excuses the case");
assert!(citation.contains("no `terminology` posture"), "{citation}");
let exception = selection_exception(&set, &bare, Some(&statement(&["EhrStatus"])), &case)
.expect("the law is decidable")
.expect("the case is excused");
assert!(matches!(exception, Exception::Guarded(_)), "{exception:?}");
}
#[test]
fn an_excused_case_records_one_cited_not_applicable_row() {
let (_, case) = selection_world();
let record = not_applicable_record(&case, "the citation");
assert_eq!(record.case, case.id);
assert_eq!(record.rows_driven, 0);
assert_eq!(record.rows_total, crate::exec::row_count(&case));
match record.rows.as_slice() {
[RowOutcome::NotApplicable { citation }] => assert_eq!(citation, "the citation"),
other => panic!("expected one cited not-applicable row, got {other:?}"),
}
}
#[test]
fn an_empty_run_reports_full_interpreter_coverage() {
let empty = RunReport::default().interpreter_coverage();
assert!((empty - 1.0).abs() < f64::EPSILON, "{empty}");
let partial = RunReport {
interpreter_run: 3,
considered: 4,
..RunReport::default()
}
.interpreter_coverage();
assert!((partial - 0.75).abs() < f64::EPSILON, "{partial}");
}
#[test]
fn a_variant_step_selects_its_own_realization() {
let base: serde_json::Value = serde_json::json!({
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"its": "its-rest",
"request": { "method": "GET", "path": "/ehr/{ehr_id}/ehr_status" },
"outcomes": { "ok": { "status": 200 } }
});
let mut at_version = base.clone();
at_version["variant"] = serde_json::json!("at_version");
at_version["applies"] = serde_json::json!({ "its_rest": ">=9.9.9" });
let mut set = ArtifactSet::default();
for (name, document) in [("plain.yaml", &base), ("variant.yaml", &at_version)] {
set.bindings.push((
std::path::PathBuf::from(name),
serde_json::from_value(document.clone()).unwrap(),
));
}
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_STATUS.get_ehr_status-at_version", "kind": "functional",
"component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrStatus"],
"flow": [
{ "step": 1, "call": "get_ehr_status", "variant": "at_version", "expect": "ok" }
]
}))
.unwrap();
let versions = crate::party::SpecVersions {
its_rest: Some("1.1.0".to_owned()),
..crate::party::SpecVersions::default()
};
let unmet = unmet_binding_floors(&set, &case, &versions);
assert_eq!(unmet.len(), 1, "{unmet:?}");
assert!(unmet[0].contains(">=9.9.9"), "{unmet:?}");
let exception = selection_exception(
&set,
&ixit(&serde_json::json!({})),
Some(&statement(&["EhrStatus"])),
&case,
)
.expect("the law is decidable")
.expect("the unmet operation floor excuses the case");
match &exception {
Exception::Unrealized(citation) => {
assert!(
citation.contains("operation version floor unmet"),
"{citation}"
);
}
other => panic!("expected an unrealized exception, got {other:?}"),
}
let unbound: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_SERVICE.create_ehr-unbound", "kind": "functional", "component": "EHR",
"sm_operation": "I_EHR_SERVICE.create_ehr",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [{ "step": 1, "call": "create_ehr", "expect": "created" }]
}))
.unwrap();
assert!(unmet_binding_floors(&set, &unbound, &versions).is_empty());
}
#[test]
fn a_malformed_dotted_call_marks_no_extension_family() {
let (set, _) = selection_world();
let malformed: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_STATUS.get_ehr_status-malformed", "kind": "functional",
"component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrStatus"],
"flow": [{ "step": 1, "call": "NOT_AN_INTERFACE.get", "expect": "ok" }]
}))
.unwrap();
assert!(extension_family(&set, &malformed).is_none());
assert!(fully_unrealized(&set, &malformed).is_none());
assert!(
unmet_binding_floors(&set, &malformed, &statement(&["EhrStatus"]).spec_versions)
.is_empty()
);
let anchorless: CaseCore = serde_json::from_value(serde_json::json!({
"id": "CONT-DV_TEXT-anchorless", "kind": "content", "component": "CONTENT",
"rm_class": "DV_TEXT",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [{ "step": 1, "call": "create_composition", "expect": "created" }]
}))
.unwrap();
assert!(extension_family(&set, &anchorless).is_none());
assert!(fully_unrealized(&set, &anchorless).is_none());
}
#[test]
fn a_fully_unrealized_case_is_excused_with_its_binding_citation() {
let unrealized: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"its": "its-rest",
"unrealized": {
"reason": "the release surfaces no such route",
"source": "SM i_ehr_status.adoc vs ITS-REST ehr.openapi.yaml",
"ambiguity": "AMB-77"
},
"request": { "method": "GET", "path": "/ehr/{ehr_id}/ehr_status" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let mut set = ArtifactSet::default();
set.bindings
.push((std::path::PathBuf::from("u.yaml"), unrealized));
let (_, case) = selection_world();
let citation = fully_unrealized(&set, &case).expect("every step is unrealized");
assert!(citation.contains("AMB-77"), "{citation}");
let exception = selection_exception(
&set,
&ixit(&serde_json::json!({})),
Some(&statement(&["EhrStatus"])),
&case,
)
.expect("the law is decidable")
.expect("an unrealized case is excused");
match &exception {
Exception::Unrealized(citation) => assert!(citation.contains("AMB-77"), "{citation}"),
other => panic!("expected an unrealized exception, got {other:?}"),
}
}
#[test]
fn the_law_routes_unclaimed_capabilities_and_extensions_apart() {
let (set, case) = selection_world();
let exception = selection_exception(
&set,
&ixit(&serde_json::json!({})),
Some(&statement(&["EhrOperations"])),
&case,
)
.expect("the law is decidable")
.expect("the ICS claims none of this case's capabilities");
match &exception {
Exception::Guarded(citation) => {
assert!(citation.contains("EhrStatus"), "{citation}");
assert!(citation.contains("master02-overview.adoc"), "{citation}");
}
other => panic!("expected a guarded exception, got {other:?}"),
}
let extension: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_PARTY_RELATIONSHIP.get_party_relationship",
"its": "its-rest",
"extension": {
"family": "party-relationship",
"reason": "the release surfaces no PARTY_RELATIONSHIP resource",
"source": "SM i_party_relationship.adoc vs ITS-REST demographic.openapi.yaml",
"ambiguity": "AMB-32"
},
"request": { "method": "GET", "path": "/demographic/party_relationship/{versioned_object_uid}" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let mut extension_set = ArtifactSet::default();
extension_set
.bindings
.push((std::path::PathBuf::from("e.yaml"), extension));
let on_extension: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-extension", "kind": "functional", "component": "DEMOGRAPHIC",
"sm_operation": "I_PARTY_RELATIONSHIP.get_party_relationship",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["PartyRelationshipOperations"],
"flow": [{ "step": 1, "call": "get_party_relationship", "expect": "ok" }]
}))
.unwrap();
let exception = selection_exception(
&extension_set,
&ixit(&serde_json::json!({})),
Some(&statement(&["EhrOperations"])),
&on_extension,
)
.expect("the law is decidable")
.expect("the ICS claims none of the extension family's capabilities");
match &exception {
Exception::Unrealized(citation) => {
assert!(citation.contains("extension realization"), "{citation}");
assert!(citation.contains("AMB-32"), "{citation}");
}
other => panic!("expected an unrealized exception, got {other:?}"),
}
assert!(
selection_exception(
&extension_set,
&ixit(&serde_json::json!({})),
Some(&statement(&["PartyRelationshipOperations"])),
&on_extension,
)
.expect("the law is decidable")
.is_none()
);
}
#[test]
fn a_provisioning_scope_needs_both_a_statement_and_a_realized_family() {
let (set, _) = selection_world();
let importing: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_STATUS.get_ehr_status-imported", "kind": "functional",
"component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrStatus"],
"requires": { "import": { "extract": "cnf.extract.one", "container": "X_VERSIONED_COMPOSITION" } },
"flow": [{ "step": 1, "call": "get_ehr_status", "expect": "ok" }]
}))
.unwrap();
assert_eq!(unservable_import(&set, None, &importing).unwrap(), None);
assert_eq!(
unservable_import(&set, Some(&statement(&["EhrStatus"])), &importing).unwrap(),
None
);
let (_, plain) = selection_world();
assert_eq!(
unservable_import(&set, Some(&statement(&["EhrStatus"])), &plain).unwrap(),
None
);
assert_eq!(
unservable_party_relationship(&set, Some(&statement(&["EhrStatus"])), &plain).unwrap(),
None
);
}
#[test]
fn only_cases_driving_the_family_contribute_its_capabilities() {
let extension: crate::model::binding::OperationBinding =
serde_json::from_value(serde_json::json!({
"sm_operation": "I_PARTY_RELATIONSHIP.get_party_relationship",
"its": "its-rest",
"extension": {
"family": "party-relationship",
"reason": "the release surfaces no PARTY_RELATIONSHIP resource",
"source": "SM i_party_relationship.adoc vs ITS-REST demographic.openapi.yaml",
"ambiguity": "AMB-32"
},
"request": { "method": "GET", "path": "/demographic/party_relationship/{versioned_object_uid}" },
"outcomes": { "ok": { "status": 200 } }
}))
.unwrap();
let (mut set, released_case) = selection_world();
set.bindings
.push((std::path::PathBuf::from("e.yaml"), extension));
let on_extension: CaseCore = serde_json::from_value(serde_json::json!({
"id": "X-extension", "kind": "functional", "component": "DEMOGRAPHIC",
"sm_operation": "I_PARTY_RELATIONSHIP.get_party_relationship",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["PartyRelationshipOperations"],
"flow": [{ "step": 1, "call": "get_party_relationship", "expect": "ok" }]
}))
.unwrap();
set.cases
.push((std::path::PathBuf::from("released.yaml"), released_case));
set.cases
.push((std::path::PathBuf::from("extension.yaml"), on_extension));
let claiming = capabilities_claiming_family(&set, "party-relationship");
assert_eq!(
claiming,
vec![CapabilityName::parse("PartyRelationshipOperations").unwrap()],
"the released-route case contributes nothing"
);
assert!(capabilities_claiming_family(&set, "ehr-extract").is_empty());
}
#[test]
fn terminology_reachability_is_judged_per_declared_namespace() {
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_STATUS.get_ehr_status-term_down", "kind": "functional",
"component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrStatus"],
"requires": { "terminology": { "served": ["SNOMED-CT"] } },
"flow": [{ "step": 1, "call": "get_ehr_status", "expect": "ok" }]
}))
.unwrap();
let lane = |reachable: bool| {
serde_json::json!({
"terminology": {
"posture": "fail_closed",
"servers": [
{ "name": "ts", "namespaces": ["SNOMED-CT"], "reachable": reachable }
]
}
})
};
let unreachable = ixit(&lane(false));
let citation = unsatisfied_terminology(&case, &unreachable)
.expect("the case needs the namespace answered");
assert!(citation.contains("declares unreachable"), "{citation}");
assert!(unsatisfied_terminology(&case, &ixit(&lane(true))).is_none());
let needs_down: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_STATUS.get_ehr_status-term_up", "kind": "functional",
"component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrStatus"],
"requires": { "terminology": { "unreachable": ["SNOMED-CT"] } },
"flow": [{ "step": 1, "call": "get_ehr_status", "expect": "ok" }]
}))
.unwrap();
let citation = unsatisfied_terminology(&needs_down, &ixit(&lane(true)))
.expect("a reachable server cannot produce the down branch");
assert!(citation.contains("declared reachable"), "{citation}");
assert!(unsatisfied_terminology(&needs_down, &ixit(&lane(false))).is_none());
let elsewhere = serde_json::json!({
"terminology": {
"posture": "fail_closed",
"servers": [{ "name": "ts", "namespaces": ["LOINC"], "reachable": true }]
}
});
assert!(
unsatisfied_terminology(&case, &ixit(&elsewhere))
.expect("no server answers for the namespace")
.contains("seeded no such namespace")
);
assert!(
unsatisfied_terminology(&needs_down, &ixit(&elsewhere))
.expect("no server answers for the namespace")
.contains("no such unreachable namespace")
);
let elsewhere_case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_STATUS.get_ehr_status-readonly", "kind": "functional",
"component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrStatus"],
"requires": { "terminology": { "served": ["SNOMED-CT"] } },
"flow": [
{ "step": 1, "call": "get_ehr_status", "on": "readonly", "expect": "ok" }
]
}))
.unwrap();
assert_eq!(
unsatisfied_terminology(&elsewhere_case, &ixit(&lane(true))),
None
);
assert_eq!(
unsatisfied_spec_profile(&elsewhere_case, &ixit(&lane(true))),
None
);
}
#[test]
fn an_undeclared_dump_location_is_collected_by_name() {
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "I_EHR_STATUS.get_ehr_status-dump", "kind": "functional", "component": "EHR",
"sm_operation": "I_EHR_STATUS.get_ehr_status",
"test_purpose": "t", "description": "d", "spec_refs": [],
"capabilities": ["EhrStatus"],
"flow": [{
"step": 1, "call": "get_ehr_status", "expect": "ok",
"with": { "path": "${ixit:dump_location}" }
}]
}))
.unwrap();
assert_eq!(
undeclared_ixit_facts(&case, &ixit(&serde_json::json!({}))),
vec!["dump_location"]
);
let declared = ixit(&serde_json::json!({ "dump_location": "/var/lib/ehr" }));
assert!(undeclared_ixit_facts(&case, &declared).is_empty());
}
#[test]
fn a_content_case_without_a_decision_table_synthesizes_unchanged() {
let case: CaseCore = serde_json::from_value(serde_json::json!({
"id": "CONT-DV_TEXT-no_table", "kind": "content", "component": "CONTENT",
"rm_class": "DV_TEXT",
"test_purpose": "t", "description": "d", "spec_refs": [],
"flow": [{ "step": 1, "call": "create_composition", "expect": "created" }]
}))
.unwrap();
let synthesized = synthesize_content_case(&case);
assert!(synthesized.parameters.is_none());
assert_eq!(synthesized.flow.len(), 1);
assert_eq!(synthesized.id, case.id);
}
}