1use haste_fhir_client::{
2 FHIRClient,
3 request::{
4 DeleteRequest, FHIRCreateRequest, FHIRDeleteInstanceRequest, FHIRDeleteSystemRequest,
5 FHIRDeleteTypeRequest, FHIRHistoryInstanceRequest, FHIRHistorySystemRequest,
6 FHIRHistoryTypeRequest, FHIRInvokeInstanceRequest, FHIRInvokeSystemRequest,
7 FHIRInvokeTypeRequest, FHIRReadRequest, FHIRRequest, FHIRResponse, FHIRTransactionRequest,
8 FHIRUpdateInstanceRequest, FHIRVersionReadRequest, HistoryRequest, HistoryResponse,
9 InvocationRequest, InvokeResponse, Operation, SearchResponse, UpdateRequest,
10 },
11 url::ParsedParameters,
12};
13use haste_fhir_model::r4::generated::{
14 resources::{
15 Resource, ResourceType, TestReport, TestReportSetup, TestReportSetupAction,
16 TestReportSetupActionAssert, TestReportSetupActionOperation, TestReportTeardown,
17 TestReportTeardownAction, TestReportTest, TestReportTestAction, TestScript,
18 TestScriptFixture, TestScriptSetup, TestScriptSetupAction, TestScriptSetupActionAssert,
19 TestScriptSetupActionOperation, TestScriptTeardown, TestScriptTeardownAction,
20 TestScriptTest, TestScriptTestAction, TestScriptVariable,
21 },
22 terminology::{
23 AssertDirectionCodes, AssertOperatorCodes, BundleType, IssueType, ReportActionResultCodes,
24 ReportResultCodes, ReportStatusCodes, TestscriptOperationCodes,
25 },
26 types::{FHIRId, FHIRMarkdown, FHIRString, Reference},
27};
28use haste_fhir_operation_error::OperationOutcomeError;
29use haste_pointer::{Key, Pointer};
30use haste_reflect::MetaValue;
31use regex::Regex;
32use std::{
33 any::Any,
34 collections::HashMap,
35 sync::{Arc, LazyLock},
36 time::Duration,
37};
38use tokio::sync::Mutex;
39
40use crate::conversion::ConvertedValue;
41
42mod conversion;
43
44#[derive(Debug)]
45pub enum TestScriptError {
46 ExecutionError(String),
47 ValidationError(String),
48 FixtureNotFound,
49 InvalidFixture,
50 OperationError(OperationOutcomeError),
51}
52
53#[derive(Debug, Clone)]
54enum Response {
55 FHIRResponse(FHIRResponse),
56 OperationError(Arc<OperationOutcomeError>),
57}
58
59#[derive(Debug)]
60enum Fixtures {
61 Resource(Resource),
62 Request(FHIRRequest),
63 Response(Response),
64}
65
66struct TestState {
68 fp_engine: haste_fhirpath::FPEngine,
69 fixtures: HashMap<String, Fixtures>,
70 latest_request: Option<FHIRRequest>,
71 latest_response: Option<Response>,
72 result: ReportResultCodes,
73}
74
75impl TestState {
76 fn new() -> Self {
77 TestState {
78 fp_engine: haste_fhirpath::FPEngine::new(),
79 fixtures: HashMap::new(),
80 latest_request: None,
81 latest_response: None,
82 result: ReportResultCodes::Pending(None),
83 }
84 }
85 fn resolve_fixture<'a>(
86 &'a self,
87 fixture_id: &str,
88 ) -> Result<&'a dyn MetaValue, TestScriptError> {
89 let fixture = self
90 .fixtures
91 .get(fixture_id)
92 .ok_or(TestScriptError::FixtureNotFound)?;
93
94 match fixture {
95 Fixtures::Resource(res) => Ok(res),
96 Fixtures::Request(req) => {
97 request_to_meta_value(req).ok_or_else(|| TestScriptError::InvalidFixture)
98 }
99 Fixtures::Response(response) => {
100 response_to_meta_value(response).ok_or_else(|| TestScriptError::InvalidFixture)
101 }
102 }
103 }
104}
105
106struct TestResult<T> {
107 pub state: Arc<Mutex<TestState>>,
108 pub value: T,
109}
110
111fn response_to_meta_value<'a>(response: &'a Response) -> Option<&'a dyn MetaValue> {
112 match response {
113 Response::FHIRResponse(fhir_response) => match fhir_response {
114 FHIRResponse::Create(res) => Some(&res.resource),
115 FHIRResponse::Read(res) => Some(&res.resource),
116 FHIRResponse::VersionRead(res) => Some(&res.resource),
117 FHIRResponse::Update(res) => Some(&res.resource),
118 FHIRResponse::Patch(res) => Some(&res.resource),
119 FHIRResponse::Batch(res) => Some(&res.resource),
120 FHIRResponse::Transaction(res) => Some(&res.resource),
121
122 FHIRResponse::Capabilities(res) => Some(&res.capabilities),
123 FHIRResponse::Search(res) => match res {
124 SearchResponse::Type(res) => Some(&res.bundle),
125 SearchResponse::System(res) => Some(&res.bundle),
126 },
127 FHIRResponse::History(res) => match res {
128 HistoryResponse::Instance(res) => Some(&res.bundle),
129 HistoryResponse::Type(res) => Some(&res.bundle),
130 HistoryResponse::System(res) => Some(&res.bundle),
131 },
132 FHIRResponse::Invoke(res) => match res {
133 InvokeResponse::Instance(res) => Some(&res.resource),
134 InvokeResponse::Type(res) => Some(&res.resource),
135 InvokeResponse::System(res) => Some(&res.resource),
136 },
137
138 FHIRResponse::Delete(_) => None,
139 },
140 Response::OperationError(op_error) => {
141 let outcome = op_error.outcome();
142 Some(outcome)
143 }
144 }
145}
146
147fn request_to_meta_value<'a>(request: &'a FHIRRequest) -> Option<&'a dyn MetaValue> {
148 match request {
149 FHIRRequest::Create(req) => Some(&req.resource),
150
151 FHIRRequest::Update(update_request) => match update_request {
152 UpdateRequest::Conditional(req) => Some(&req.resource),
153 UpdateRequest::Instance(req) => Some(&req.resource),
154 },
155
156 FHIRRequest::Batch(req) => Some(&req.resource),
157 FHIRRequest::Transaction(req) => Some(&req.resource),
158 FHIRRequest::Invocation(req) => match req {
159 haste_fhir_client::request::InvocationRequest::Instance(req) => Some(&req.parameters),
160 haste_fhir_client::request::InvocationRequest::Type(req) => Some(&req.parameters),
161 haste_fhir_client::request::InvocationRequest::System(req) => Some(&req.parameters),
162 },
163 FHIRRequest::Read(_)
164 | FHIRRequest::VersionRead(_)
165 | FHIRRequest::Compartment(_)
166 | FHIRRequest::Patch(_)
167 | FHIRRequest::Delete(_)
168 | FHIRRequest::Capabilities
169 | FHIRRequest::Search(_)
170 | FHIRRequest::History(_) => None,
171 }
172}
173
174fn associate_request_response_variables(
175 state: &mut TestState,
176 operation: &TestScriptSetupActionOperation,
177 request: FHIRRequest,
178 response: Response,
179) {
180 if let Some(request_var) = operation
181 .requestId
182 .as_ref()
183 .and_then(|id| id.value.as_ref())
184 {
185 state
187 .fixtures
188 .insert(request_var.clone(), Fixtures::Request(request.clone()));
189 }
190
191 if let Some(response_var) = operation
192 .responseId
193 .as_ref()
194 .and_then(|id| id.value.as_ref())
195 {
196 state
198 .fixtures
199 .insert(response_var.clone(), Fixtures::Response(response.clone()));
200 }
201
202 state.latest_request = Some(request);
203 state.latest_response = Some(response);
204}
205
206fn derive_resource_type(
208 operation: &TestScriptSetupActionOperation,
209 target: Option<&dyn MetaValue>,
210 path: &str,
211) -> Result<ResourceType, TestScriptError> {
212 if let Some(operation_resource_type) = operation.resource.as_ref() {
213 let string_type: Option<String> = operation_resource_type.as_ref().into();
214 ResourceType::try_from(string_type.unwrap_or_default()).map_err(|_| {
215 TestScriptError::ExecutionError(format!(
216 "Unsupported resource type '{:?}' for operation at '{}'.",
217 operation_resource_type.as_ref(),
218 path
219 ))
220 })
221 } else if let Some(target) = target {
222 ResourceType::try_from(target.typename()).map_err(|_| {
223 TestScriptError::ExecutionError(format!(
224 "Unsupported resource type '{}' for operation at '{}'.",
225 target.typename(),
226 path
227 ))
228 })
229 } else {
230 Err(TestScriptError::ExecutionError(format!(
231 "Failed to derive resource type for operation at '{}'.",
232 path
233 )))
234 }
235}
236
237static EXPRESSION_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\{([^}]*)\}").unwrap());
238
239async fn get_variable(
240 state: &TestState,
241 variables: &Vec<TestScriptVariable>,
242 variable_id: &str,
243) -> Result<ConvertedValue, TestScriptError> {
244 let Some(variable) = variables
245 .iter()
246 .find(|v| v.name.value.as_ref().map(|s| s.as_str()) == Some(variable_id))
247 else {
248 return Err(TestScriptError::ExecutionError(format!(
249 "Variable with id '{}' not found.",
250 variable_id
251 )));
252 };
253
254 if let Some(expression) = variable
255 .expression
256 .as_ref()
257 .and_then(|exp| exp.value.as_ref())
258 {
259 let values =
260 if let Some(source_id) = variable.sourceId.as_ref().and_then(|id| id.value.as_ref()) {
261 let source = state.resolve_fixture(source_id)?;
262 vec![source]
263 } else {
264 vec![]
265 };
266
267 let eval_result = state
268 .fp_engine
269 .evaluate(expression, values)
270 .await
271 .map_err(|e| {
272 TestScriptError::ExecutionError(format!(
273 "Failed to evaluate FHIRPath expression for variable '{}': {}",
274 variable_id, e
275 ))
276 })?;
277
278 let converted_values = eval_result
279 .iter()
280 .map(|d| {
281 conversion::convert_meta_value(d).ok_or_else(|| {
282 TestScriptError::ExecutionError(format!(
283 "Failed to convert comparison fixture value '{}'.",
284 d.typename()
285 ))
286 })
287 })
288 .collect::<Result<Vec<_>, TestScriptError>>()?;
289
290 if converted_values.len() == 1 {
291 Ok(converted_values.into_iter().next().unwrap())
292 } else {
293 Err(TestScriptError::ExecutionError(format!(
294 "Variable '{}' evaluation returned multiple values; only single value supported.",
295 variable_id
296 )))
297 }
298 } else {
299 return Err(TestScriptError::ExecutionError(format!(
300 "Only support variable with expression for variable id '{}'.",
301 variable_id
302 )));
303 }
304}
305
306async fn evaluate_variable(
307 state: &TestState,
308 pointer: Pointer<TestScript, TestScript>,
309 value: &str,
310) -> Result<String, TestScriptError> {
311 let mut result = value.to_string();
312 let variable_pointer =
313 pointer.descend::<Vec<TestScriptVariable>>(&Key::Field("variable".to_string()));
314 let default_variables = vec![];
315
316 let variables = if let Some(pointer) = variable_pointer.as_ref() {
317 pointer.value().unwrap_or(&default_variables)
318 } else {
319 &default_variables
320 };
321
322 for reg_match in EXPRESSION_REGEX.captures_iter(value) {
323 let full_match = reg_match.get(0).map(|m| m.as_str()).unwrap_or("");
324 let Some(variable_id) = reg_match.get(1).map(|m| m.as_str()) else {
325 return Err(TestScriptError::ExecutionError(format!(
326 "Invalid variable expression in '{}'.",
327 value
328 )));
329 };
330
331 let variable = get_variable(state, variables, variable_id).await?;
332 result = result.replace(full_match, variable.to_string().as_str());
333 }
334
335 Ok(result)
336}
337
338async fn testscript_operation_to_fhir_request(
339 state: &TestState,
340 pointer: &Pointer<TestScript, TestScriptSetupActionOperation>,
341) -> Result<FHIRRequest, TestScriptError> {
342 let operation = pointer.value().ok_or_else(|| {
343 TestScriptError::ExecutionError(format!(
344 "Failed to retrieve TestScript operation at '{}'.",
345 pointer.path()
346 ))
347 })?;
348
349 let operation_type = operation
350 .type_
351 .as_ref()
352 .and_then(|t| t.code.as_ref())
353 .and_then(|c| c.value.clone());
354
355 if operation_type == (&TestscriptOperationCodes::Read(None)).into() {
356 let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) else {
357 return Err(TestScriptError::ExecutionError(format!(
358 "Read operation requires targetId at '{}'.",
359 pointer.path()
360 )));
361 };
362
363 let target = state.resolve_fixture(target_id)?;
364
365 Ok(FHIRRequest::Read(FHIRReadRequest {
366 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
367 id: target
368 .get_field("id")
369 .ok_or_else(|| {
370 TestScriptError::ExecutionError(format!(
371 "Target fixture '{}' does not have an 'id' field.",
372 target_id
373 ))
374 })?
375 .as_any()
376 .downcast_ref::<String>()
377 .cloned()
378 .unwrap_or_default(),
379 }))
380 } else if operation_type == (&TestscriptOperationCodes::Vread(None)).into() {
381 let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) else {
382 return Err(TestScriptError::ExecutionError(format!(
383 "Version Read operation requires targetId at '{}'.",
384 pointer.path()
385 )));
386 };
387 let target = state.resolve_fixture(target_id)?;
388
389 let id = target
390 .get_field("id")
391 .ok_or_else(|| {
392 TestScriptError::ExecutionError(format!(
393 "Target fixture '{}' does not have an 'id' field.",
394 target_id
395 ))
396 })?
397 .as_any()
398 .downcast_ref::<String>()
399 .cloned()
400 .unwrap_or_default();
401 let version_id = target
402 .get_field("meta")
403 .and_then(|meta| meta.get_field("versionId"))
404 .ok_or_else(|| {
405 TestScriptError::ExecutionError(format!(
406 "Target fixture '{}' does not have an 'versionId' field.",
407 target_id
408 ))
409 })?
410 .as_any()
411 .downcast_ref::<Box<FHIRId>>()
412 .cloned()
413 .and_then(|v| v.value)
414 .ok_or_else(|| {
415 TestScriptError::ExecutionError(format!(
416 "Target fixture '{}' does not have an 'versionId' field.",
417 target_id
418 ))
419 })?;
420
421 Ok(FHIRRequest::VersionRead(FHIRVersionReadRequest {
422 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
423 id: id,
424 version_id: version_id.into(),
425 }))
426 } else if operation_type == (&TestscriptOperationCodes::Search(None)).into() {
427 let query_string = operation
428 .params
429 .as_ref()
430 .and_then(|p| p.value.as_ref())
431 .cloned()
432 .unwrap_or_default();
433
434 let processed_query = ParsedParameters::try_from(
435 evaluate_variable(state, pointer.root(), &query_string)
436 .await?
437 .as_str(),
438 )
439 .map_err(|e| {
440 TestScriptError::ExecutionError(format!(
441 "Failed to parse parameters for History operation at '{}': {}",
442 pointer.path(),
443 e
444 ))
445 })?;
446
447 if let Ok(resource_type) = derive_resource_type(operation, None, pointer.path()) {
448 Ok(FHIRRequest::Search(
449 haste_fhir_client::request::SearchRequest::Type(
450 haste_fhir_client::request::FHIRSearchTypeRequest {
451 resource_type,
452 parameters: processed_query,
453 },
454 ),
455 ))
456 } else {
457 Ok(FHIRRequest::Search(
458 haste_fhir_client::request::SearchRequest::System(
459 haste_fhir_client::request::FHIRSearchSystemRequest {
460 parameters: processed_query,
461 },
462 ),
463 ))
464 }
465 } else if operation_type == (&TestscriptOperationCodes::History(None)).into() {
466 let query_string = operation
467 .params
468 .as_ref()
469 .and_then(|p| p.value.as_ref())
470 .cloned()
471 .unwrap_or_default();
472
473 let processed_query = ParsedParameters::try_from(
474 evaluate_variable(state, pointer.root(), &query_string)
475 .await?
476 .as_str(),
477 )
478 .map_err(|e| {
479 TestScriptError::ExecutionError(format!(
480 "Failed to parse parameters for History operation at '{}': {}",
481 pointer.path(),
482 e
483 ))
484 })?;
485
486 if let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) {
487 let target = state.resolve_fixture(target_id)?;
488
489 return Ok(FHIRRequest::History(HistoryRequest::Instance(
490 FHIRHistoryInstanceRequest {
491 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
492 id: target
493 .get_field("id")
494 .ok_or_else(|| {
495 TestScriptError::ExecutionError(format!(
496 "Target fixture '{}' does not have an 'id' field.",
497 target_id
498 ))
499 })?
500 .as_any()
501 .downcast_ref::<String>()
502 .cloned()
503 .unwrap_or_default(),
504 parameters: processed_query,
505 },
506 )));
507 } else if operation.resource.is_some() {
508 let resource_type = derive_resource_type(operation, None, pointer.path())?;
509 return Ok(FHIRRequest::History(HistoryRequest::Type(
510 FHIRHistoryTypeRequest {
511 resource_type,
512 parameters: processed_query,
513 },
514 )));
515 } else {
516 return Ok(FHIRRequest::History(HistoryRequest::System(
517 FHIRHistorySystemRequest {
518 parameters: processed_query,
519 },
520 )));
521 }
522 } else if operation_type == (&TestscriptOperationCodes::Transaction(None)).into() {
523 let Some(source_id) = operation.sourceId.as_ref().and_then(|id| id.value.as_ref()) else {
524 return Err(TestScriptError::ExecutionError(format!(
525 "Transaction operation requires sourceId at '{}'.",
526 pointer.path()
527 )));
528 };
529
530 let source = state.resolve_fixture(source_id)?;
531 let resource = (source as &dyn Any)
532 .downcast_ref::<Resource>()
533 .cloned()
534 .ok_or_else(|| {
535 TestScriptError::ExecutionError(format!(
536 "Target fixture '{}' is not a Resource.",
537 source_id
538 ))
539 })?;
540
541 match resource {
542 Resource::Bundle(bundle) => {
543 if !matches!(bundle.type_.as_ref(), BundleType::Transaction(_)) {
544 return Err(TestScriptError::ExecutionError(format!(
545 "Fixture must be a transaction bundle for transaction operations for sourceId '{}'.",
546 source_id
547 )));
548 }
549
550 Ok(FHIRRequest::Transaction(FHIRTransactionRequest {
551 resource: bundle,
552 }))
553 }
554
555 _ => Err(TestScriptError::ExecutionError(format!(
556 "Fixture '{}' is not a transaction Bundle resource.",
557 source_id
558 ))),
559 }
560 } else if operation_type == (&TestscriptOperationCodes::Create(None)).into() {
561 let Some(source_id) = operation.sourceId.as_ref().and_then(|id| id.value.as_ref()) else {
562 return Err(TestScriptError::ExecutionError(format!(
563 "Create operation requires sourceId at '{}'.",
564 pointer.path()
565 )));
566 };
567
568 let source = state.resolve_fixture(source_id)?;
569 let resource = (source as &dyn Any)
570 .downcast_ref::<Resource>()
571 .cloned()
572 .ok_or_else(|| {
573 TestScriptError::ExecutionError(format!(
574 "Target fixture '{}' is not a Resource.",
575 source_id
576 ))
577 })?;
578
579 Ok(FHIRRequest::Create(FHIRCreateRequest {
580 resource_type: derive_resource_type(operation, Some(source), pointer.path())?,
581 resource: resource,
582 }))
583 } else if operation_type == (&TestscriptOperationCodes::Update(None)).into() {
584 let Some(source_id) = operation.sourceId.as_ref().and_then(|id| id.value.as_ref()) else {
585 return Err(TestScriptError::ExecutionError(format!(
586 "Update operation requires sourceId at '{}'.",
587 pointer.path()
588 )));
589 };
590 let source = state.resolve_fixture(source_id)?;
591 let resource = (source as &dyn Any)
592 .downcast_ref::<Resource>()
593 .cloned()
594 .ok_or_else(|| {
595 TestScriptError::ExecutionError(format!(
596 "Source fixture '{}' is not a Resource.",
597 source_id
598 ))
599 })?;
600
601 let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) else {
602 return Err(TestScriptError::ExecutionError(format!(
603 "Update operation requires targetId at '{}'.",
604 pointer.path()
605 )));
606 };
607
608 let target = state.resolve_fixture(target_id)?;
609 let target_resource = (target as &dyn Any)
610 .downcast_ref::<Resource>()
611 .cloned()
612 .ok_or_else(|| {
613 TestScriptError::ExecutionError(format!(
614 "Source fixture '{}' is not a Resource.",
615 source_id
616 ))
617 })?;
618
619 Ok(FHIRRequest::Update(UpdateRequest::Instance(
620 FHIRUpdateInstanceRequest {
621 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
622 id: target_resource
623 .get_field("id")
624 .ok_or_else(|| {
625 TestScriptError::ExecutionError(format!(
626 "Source fixture '{}' does not have an 'id' field.",
627 source_id
628 ))
629 })?
630 .as_any()
631 .downcast_ref::<String>()
632 .cloned()
633 .unwrap_or_default(),
634 resource: resource,
635 },
636 )))
637 } else if operation_type == (&TestscriptOperationCodes::Delete(None)).into() {
638 let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) else {
639 return Err(TestScriptError::ExecutionError(format!(
640 "Delete operation requires targetId at '{}'.",
641 pointer.path()
642 )));
643 };
644
645 let target = state.resolve_fixture(target_id)?;
646
647 Ok(FHIRRequest::Delete(DeleteRequest::Instance(
648 FHIRDeleteInstanceRequest {
649 resource_type: derive_resource_type(operation, Some(target), pointer.path())?,
650 id: target
651 .get_field("id")
652 .ok_or_else(|| {
653 TestScriptError::ExecutionError(format!(
654 "Target fixture '{}' does not have an 'id' field.",
655 target_id
656 ))
657 })?
658 .as_any()
659 .downcast_ref::<String>()
660 .cloned()
661 .unwrap_or_default(),
662 },
663 )))
664 } else if operation_type == (&TestscriptOperationCodes::DeleteCondMultiple(None)).into() {
665 let delete_parameters = ParsedParameters::try_from(
666 operation
667 .params
668 .as_ref()
669 .and_then(|p| p.value.as_ref())
670 .cloned()
671 .unwrap_or("".to_string())
672 .as_str(),
673 )
674 .map_err(|e| {
675 TestScriptError::ExecutionError(format!(
676 "Failed to parse parameters for DeleteCondMultiple operation at '{}': {}",
677 pointer.path(),
678 e
679 ))
680 })?;
681 if operation.resource.is_some() {
682 Ok(FHIRRequest::Delete(DeleteRequest::Type(
683 FHIRDeleteTypeRequest {
684 resource_type: derive_resource_type(operation, None, pointer.path())?,
685 parameters: delete_parameters,
686 },
687 )))
688 } else {
689 Ok(FHIRRequest::Delete(DeleteRequest::System(
690 FHIRDeleteSystemRequest {
691 parameters: delete_parameters,
692 },
693 )))
694 }
695 } else if operation_type == Some("invoke".to_string()) {
696 let Some(op_code) = operation.url.as_ref().and_then(|u| u.value.as_ref()) else {
697 return Err(TestScriptError::ExecutionError(format!(
698 "Invoke operation requires url at '{}' which is used for the operation code.",
699 pointer.path()
700 )));
701 };
702
703 let op_code = Operation::new(op_code).map_err(|_e| {
704 TestScriptError::ExecutionError(format!(
705 "Invalid operation code for invoke operation '{}'",
706 op_code
707 ))
708 })?;
709
710 let Some(source_id) = operation.sourceId.as_ref().and_then(|id| id.value.as_ref()) else {
711 return Err(TestScriptError::ExecutionError(format!(
712 "Invoke operation requires sourceId at '{}'.",
713 pointer.path()
714 )));
715 };
716 let source = state.resolve_fixture(source_id)?;
717 let Resource::Parameters(parameters) = (source as &dyn Any)
718 .downcast_ref::<Resource>()
719 .cloned()
720 .ok_or_else(|| {
721 TestScriptError::ExecutionError(format!(
722 "Source fixture '{}' is not a Resource.",
723 source_id
724 ))
725 })?
726 else {
727 return Err(TestScriptError::ExecutionError(format!(
728 "Source fixture '{}' is not a Parameters resource.",
729 source_id
730 )));
731 };
732
733 if let Some(target_id) = operation.targetId.as_ref().and_then(|id| id.value.as_ref()) {
734 let target = state.resolve_fixture(target_id)?;
735 let target_resource = (target as &dyn Any)
736 .downcast_ref::<Resource>()
737 .cloned()
738 .ok_or_else(|| {
739 TestScriptError::ExecutionError(format!(
740 "Source fixture '{}' is not a Resource.",
741 source_id
742 ))
743 })?;
744 let target_id = target_resource
745 .get_field("id")
746 .ok_or_else(|| {
747 TestScriptError::ExecutionError(format!(
748 "Source fixture '{}' does not have an 'id' field.",
749 source_id
750 ))
751 })?
752 .as_any()
753 .downcast_ref::<String>()
754 .cloned()
755 .unwrap_or_default();
756 let resource_type = derive_resource_type(operation, Some(target), pointer.path())?;
757
758 Ok(FHIRRequest::Invocation(InvocationRequest::Instance(
759 FHIRInvokeInstanceRequest {
760 operation: op_code,
761 resource_type,
762 id: target_id,
763 parameters,
764 },
765 )))
766 } else if let Ok(resource_type) = derive_resource_type(operation, None, pointer.path()) {
767 Ok(FHIRRequest::Invocation(InvocationRequest::Type(
768 FHIRInvokeTypeRequest {
769 operation: op_code,
770 resource_type,
771 parameters,
772 },
773 )))
774 } else {
775 Ok(FHIRRequest::Invocation(InvocationRequest::System(
776 FHIRInvokeSystemRequest {
777 operation: op_code,
778 parameters,
779 },
780 )))
781 }
782 } else {
783 Err(TestScriptError::ExecutionError(format!(
784 "Unsupported TestScript operation type: {:?} at '{}'.",
785 operation_type,
786 pointer.path()
787 )))
788 }
789}
790
791async fn run_operation<CTX, Client: FHIRClient<CTX, OperationOutcomeError>>(
792 client: &Client,
793 ctx: CTX,
794 state: Arc<Mutex<TestState>>,
795 pointer: Pointer<TestScript, TestScriptSetupActionOperation>,
796 options: Arc<TestRunnerOptions>,
797) -> Result<TestResult<TestReportSetupActionOperation>, TestScriptError> {
798 let operation = pointer.value().ok_or_else(|| {
799 TestScriptError::ExecutionError(format!(
800 "Failed to retrieve TestScript operation at '{}'.",
801 pointer.path()
802 ))
803 })?;
804
805 let mut state_guard = state.lock().await;
806 let fhir_request = testscript_operation_to_fhir_request(&state_guard, &pointer).await?;
807 let fhir_response = client.request(ctx, fhir_request.clone()).await;
808 if let Some(wait_duration) = options.wait_between_operations {
809 tokio::time::sleep(wait_duration).await;
810 }
811
812 match fhir_response {
813 Ok(fhir_response) => {
814 associate_request_response_variables(
815 &mut state_guard,
816 operation,
817 fhir_request,
818 Response::FHIRResponse(fhir_response),
819 );
820
821 drop(state_guard);
822
823 Ok(TestResult {
824 state: state.clone(),
825 value: TestReportSetupActionOperation {
826 result: Box::new(ReportActionResultCodes::Pass(None)),
827 ..Default::default()
828 },
829 })
830 }
831 Err(op_error) => {
832 let op_error = Arc::new(op_error);
833 tracing::warn!("Operation at '{}' failed: {}", pointer.path(), op_error);
834 associate_request_response_variables(
835 &mut state_guard,
836 operation,
837 fhir_request,
838 Response::OperationError(op_error.clone()),
839 );
840
841 Ok(TestResult {
842 state: state.clone(),
843 value: TestReportSetupActionOperation {
844 result: Box::new(ReportActionResultCodes::Fail(None)),
845 message: Some(Box::new(FHIRMarkdown {
846 value: Some(format!("Operation failed: {}", op_error)),
847 ..Default::default()
848 })),
849 ..Default::default()
850 },
851 })
852 }
853 }
854}
855
856static DEFAULT_DIRECTION: LazyLock<Box<AssertDirectionCodes>> =
857 LazyLock::new(|| Box::new(AssertDirectionCodes::Response(None)));
858
859async fn get_source<'a>(
860 state: &'a TestState,
861 assertion: &TestScriptSetupActionAssert,
862) -> Result<Option<&'a dyn MetaValue>, TestScriptError> {
863 if let Some(source_id) = assertion.sourceId.as_ref().and_then(|id| id.value.as_ref()) {
864 let source = state.resolve_fixture(source_id)?;
865 Ok(Some(source))
866 } else {
867 match assertion
868 .direction
869 .as_ref()
870 .unwrap_or(&DEFAULT_DIRECTION)
871 .as_ref()
872 {
873 AssertDirectionCodes::Request(_) => {
874 if let Some(request) = state.latest_request.as_ref() {
875 request_to_meta_value(request)
876 .ok_or_else(|| TestScriptError::InvalidFixture)
877 .map(Some)
878 } else {
879 Ok(None)
880 }
881 }
882 AssertDirectionCodes::Response(_) => {
883 if let Some(response) = state.latest_response.as_ref() {
884 response_to_meta_value(response)
885 .ok_or_else(|| TestScriptError::InvalidFixture)
886 .map(Some)
887 } else {
888 Ok(None)
889 }
890 }
891 AssertDirectionCodes::Null(_) => Err(TestScriptError::ExecutionError(
892 "Assert direction cannot be 'null' when sourceId is not provided.".to_string(),
893 )),
894 }
895 }
896}
897
898fn evaluate_operator(
899 operator: &Box<AssertOperatorCodes>,
900 a: &Vec<conversion::ConvertedValue>,
901 b: &Vec<conversion::ConvertedValue>,
902) -> bool {
903 match operator.as_ref() {
904 AssertOperatorCodes::Equals(_) | AssertOperatorCodes::Null(_) => a == b,
905 AssertOperatorCodes::NotEquals(_) => !(a == b),
906
907 AssertOperatorCodes::Contains(_) => {
908 if a.len() != 1 || b.len() != 1 {
909 return false;
910 }
911
912 match (&a[0], &b[0]) {
913 (ConvertedValue::String(a_str), ConvertedValue::String(b_str)) => {
914 a_str.contains(b_str)
915 }
916 _ => false,
917 }
918 }
919 AssertOperatorCodes::Empty(_) => todo!("Empty operator not implemented"),
920 AssertOperatorCodes::Eval(_) => todo!("Eval operator not implemented"),
921 AssertOperatorCodes::GreaterThan(_) => todo!("GreaterThan operator not implemented"),
922 AssertOperatorCodes::In(_) => todo!("In operator not implemented"),
923 AssertOperatorCodes::LessThan(_) => todo!("LessThan operator not implemented"),
924 AssertOperatorCodes::NotContains(_) => todo!("NotContains operator not implemented"),
925 AssertOperatorCodes::NotEmpty(_) => todo!("NotEmpty operator not implemented"),
926 AssertOperatorCodes::NotIn(_) => todo!("NotIn operator not implemented"),
927 }
928 }
930
931static DEFAULT_EQUAL_OPERATOR: LazyLock<Box<AssertOperatorCodes>> =
932 LazyLock::new(|| Box::new(AssertOperatorCodes::Equals(None)));
933
934async fn derive_comparison_to(
935 state: &TestState,
936 assertion: &TestScriptSetupActionAssert,
937) -> Result<Vec<ConvertedValue>, TestScriptError> {
938 if let Some(comparision_fixture_id) = assertion
939 .compareToSourceId
940 .as_ref()
941 .and_then(|c| c.value.as_ref())
942 {
943 let comparison_fixture = state.resolve_fixture(comparision_fixture_id)?;
944
945 let Some(comparison_expression) = assertion
946 .compareToSourceExpression
947 .as_ref()
948 .and_then(|exp| exp.value.as_ref())
949 else {
950 return Err(TestScriptError::ExecutionError(
951 "compareToSourceExpression is required when compareToSourceId is provided."
952 .to_string(),
953 ));
954 };
955
956 let result = state
957 .fp_engine
958 .evaluate(comparison_expression, vec![comparison_fixture])
959 .await
960 .map_err(|e| {
961 TestScriptError::ExecutionError(format!(
962 "FHIRPath evaluation error for comparison fixture '{}': {}",
963 comparision_fixture_id, e
964 ))
965 })?;
966
967 result
968 .iter()
969 .map(|d| {
970 conversion::convert_meta_value(d).ok_or_else(|| {
971 TestScriptError::ExecutionError(format!(
972 "Failed to convert comparison fixture value '{}'.",
973 d.typename()
974 ))
975 })
976 })
977 .collect::<Result<Vec<_>, TestScriptError>>()
978 } else if let Some(value) = assertion.value.as_ref().and_then(|v| v.value.as_ref())
979 && let Some(converted_value) = conversion::convert_string_value(value.as_ref())
980 {
981 Ok(vec![converted_value])
982 } else {
983 Err(TestScriptError::ExecutionError(
984 "Failed to derive comparison value for assertion.".to_string(),
985 ))
986 }
987}
988
989async fn run_assertion(
992 state: Arc<Mutex<TestState>>,
993 pointer: Pointer<TestScript, TestScriptSetupActionAssert>,
994) -> Result<TestResult<TestReportSetupActionAssert>, TestScriptError> {
995 let assertion = pointer.value().ok_or_else(|| {
996 TestScriptError::ExecutionError(format!(
997 "Failed to retrieve TestScript assertion at '{}'.",
998 pointer.path()
999 ))
1000 })?;
1001
1002 let mut state_guard = state.lock().await;
1003
1004 let Some(source) = get_source(&*state_guard, assertion).await? else {
1005 return Err(TestScriptError::ExecutionError(format!(
1006 "Failed to resolve source for assertion at '{}'.",
1007 pointer.path()
1008 )));
1009 };
1010
1011 let operator = assertion
1012 .operator
1013 .as_ref()
1014 .unwrap_or(&*DEFAULT_EQUAL_OPERATOR);
1015
1016 if assertion.resource.is_some() {
1017 let resource_string = assertion
1018 .resource
1019 .as_ref()
1020 .and_then(|r| {
1021 let string_type: Option<String> = r.as_ref().into();
1022 string_type
1023 })
1024 .unwrap_or("".to_string());
1025
1026 let operation_evaluation_result = evaluate_operator(
1027 operator,
1028 &vec![conversion::ConvertedValue::String(resource_string.clone())],
1029 &vec![conversion::ConvertedValue::String(
1030 source.typename().to_string(),
1031 )],
1032 );
1033 if !operation_evaluation_result {
1034 tracing::error!(
1035 "Assertion at '{}' failed: resource type '{}' does not match '{}'.",
1036 pointer.path(),
1037 resource_string,
1038 source.typename()
1039 );
1040
1041 state_guard.result = ReportResultCodes::Fail(None);
1042 return Ok(TestResult {
1043 state: state.clone(),
1044 value: TestReportSetupActionAssert {
1045 result: Box::new(ReportActionResultCodes::Fail(None)),
1046 ..Default::default()
1047 },
1048 });
1049 }
1050 }
1051 if let Some(expression) = assertion.expression.as_ref().and_then(|e| e.value.as_ref()) {
1052 let comparison_to = derive_comparison_to(&state_guard, assertion).await?;
1053
1054 let Ok(result) = state_guard
1055 .fp_engine
1056 .evaluate(expression, vec![source])
1057 .await
1058 else {
1059 tracing::error!(
1060 "Assertion at '{}' failed: FHIRPath expression '{}' failed to evaluate.",
1061 expression,
1062 pointer.path()
1063 );
1064
1065 state_guard.result = ReportResultCodes::Fail(None);
1066 return Err(TestScriptError::ExecutionError(format!(
1067 "FHIRPath failed to evaluate at '{}' error.",
1068 pointer.path()
1069 )));
1070 };
1071
1072 let converted_values = result
1073 .iter()
1074 .filter_map(|v| conversion::convert_meta_value(v))
1075 .collect::<Vec<_>>();
1076
1077 let operation_evaluation_result =
1078 evaluate_operator(operator, &converted_values, &comparison_to);
1079
1080 if !operation_evaluation_result {
1081 tracing::error!(
1082 "Assertion at '{}' failed: '{:?}' {:?} '{:?}'.",
1083 pointer.path(),
1084 converted_values,
1085 operator,
1086 comparison_to
1087 );
1088
1089 state_guard.result = ReportResultCodes::Fail(None);
1090 return Ok(TestResult {
1091 state: state.clone(),
1092 value: TestReportSetupActionAssert {
1093 result: Box::new(ReportActionResultCodes::Fail(None)),
1094 ..Default::default()
1095 },
1096 });
1097 }
1098 }
1099
1100 return Ok(TestResult {
1101 state: state.clone(),
1102 value: TestReportSetupActionAssert {
1103 result: Box::new(ReportActionResultCodes::Pass(None)),
1104 ..Default::default()
1105 },
1106 });
1107}
1108
1109async fn run_action<CTX, Client: FHIRClient<CTX, OperationOutcomeError>>(
1110 client: &Client,
1111 ctx: CTX,
1112 state: Arc<Mutex<TestState>>,
1113 pointer: Pointer<TestScript, TestScriptTestAction>,
1114 options: Arc<TestRunnerOptions>,
1115) -> Result<TestResult<TestReportSetupAction>, TestScriptError> {
1116 tracing::info!("Running TestScript action at path: {}", pointer.path());
1117 let action = pointer.value().ok_or_else(|| {
1118 TestScriptError::ExecutionError(format!(
1119 "Failed to retrieve TestScript action at '{}'.",
1120 pointer.path()
1121 ))
1122 })?;
1123
1124 if action.operation.is_some() {
1127 let Some(operation_pointer) =
1128 pointer.descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1129 else {
1130 return Err(TestScriptError::ExecutionError(format!(
1131 "Failed to retrieve TestScript operation at '{}'.",
1132 pointer.path()
1133 )));
1134 };
1135
1136 let result = run_operation(client, ctx, state, operation_pointer, options).await?;
1137
1138 Ok(TestResult {
1139 state: result.state,
1140 value: TestReportSetupAction {
1141 operation: Some(result.value),
1142 ..Default::default()
1143 },
1144 })
1145 } else if action.assert.is_some() {
1146 let Some(assertion_pointer) =
1147 pointer.descend::<TestScriptSetupActionAssert>(&Key::Field("assert".to_string()))
1148 else {
1149 return Err(TestScriptError::ExecutionError(format!(
1150 "Failed to retrieve TestScript assertion at '{}'.",
1151 pointer.path()
1152 )));
1153 };
1154
1155 let assertion = run_assertion(state, assertion_pointer).await?;
1156
1157 Ok(TestResult {
1158 state: assertion.state,
1159 value: TestReportSetupAction {
1160 assert: Some(assertion.value),
1161 ..Default::default()
1162 },
1163 })
1164 } else {
1165 Err(TestScriptError::ExecutionError(format!(
1166 "TestScript action must have either an operation or an assert at '{}'.",
1167 pointer.path()
1168 )))
1169 }
1170}
1171
1172async fn run_setup_action<CTX, Client: FHIRClient<CTX, OperationOutcomeError>>(
1173 client: &Client,
1174 ctx: CTX,
1175 state: Arc<Mutex<TestState>>,
1176 pointer: Pointer<TestScript, TestScriptSetupAction>,
1177 options: Arc<TestRunnerOptions>,
1178) -> Result<TestResult<TestReportSetupAction>, TestScriptError> {
1179 let action = pointer.value().ok_or_else(|| {
1180 TestScriptError::ExecutionError(format!(
1181 "Failed to retrieve TestScript action at '{}'.",
1182 pointer.path()
1183 ))
1184 })?;
1185
1186 tracing::info!("Running TestScript action at path: {}", pointer.path());
1187
1188 if action.operation.is_some() {
1191 let Some(operation_pointer) =
1192 pointer.descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1193 else {
1194 return Err(TestScriptError::ExecutionError(format!(
1195 "Failed to retrieve TestScript operation at '{}'.",
1196 pointer.path()
1197 )));
1198 };
1199
1200 let result = run_operation(client, ctx, state, operation_pointer, options).await?;
1201
1202 Ok(TestResult {
1203 state: result.state,
1204 value: TestReportSetupAction {
1205 operation: Some(result.value),
1206 ..Default::default()
1207 },
1208 })
1209 } else if action.assert.is_some() {
1210 let Some(assertion_pointer) =
1211 pointer.descend::<TestScriptSetupActionAssert>(&Key::Field("assert".to_string()))
1212 else {
1213 return Err(TestScriptError::ExecutionError(format!(
1214 "Failed to retrieve TestScript assertion at '{}'.",
1215 pointer.path()
1216 )));
1217 };
1218
1219 let assertion = run_assertion(state, assertion_pointer).await?;
1220
1221 Ok(TestResult {
1222 state: assertion.state,
1223 value: TestReportSetupAction {
1224 assert: Some(assertion.value),
1225 ..Default::default()
1226 },
1227 })
1228 } else {
1229 Err(TestScriptError::ExecutionError(format!(
1230 "TestScript action must have either an operation or an assert at '{}'.",
1231 pointer.path()
1232 )))
1233 }
1234}
1235
1236async fn setup_fixtures<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1237 client: &Client,
1238 ctx: CTX,
1239 state: Arc<Mutex<TestState>>,
1240 pointer: Pointer<TestScript, TestScript>,
1241 _options: Arc<TestRunnerOptions>,
1242) -> Result<Arc<Mutex<TestState>>, OperationOutcomeError> {
1243 let mut state_lock = state.lock().await;
1244
1245 let Some(fixtures_pointer) =
1246 pointer.descend::<Vec<TestScriptFixture>>(&Key::Field("fixture".to_string()))
1247 else {
1248 return Ok(state.clone());
1249 };
1250
1251 let Some(fixtures) = fixtures_pointer.value() else {
1252 return Ok(state.clone());
1253 };
1254
1255 for fixture in fixtures.iter() {
1256 if let Some(reference_string) = fixture
1257 .resource
1258 .as_ref()
1259 .and_then(|r| r.reference.as_ref())
1260 .and_then(|refe| refe.value.as_ref())
1261 {
1262 let resolved_resource = if reference_string.starts_with('#')
1263 && let Some(contained) =
1264 pointer.descend::<Vec<Box<Resource>>>(&Key::Field("contained".to_string()))
1265 && let Some(contained) = contained.value()
1266 {
1267 let local_id = &reference_string[1..];
1268 let Some(resource) = contained.iter().find(|res| {
1269 if let Some(id) = res.get_field("id")
1270 && let Some(id) = id.as_any().downcast_ref::<String>()
1271 {
1272 id.as_str() == local_id
1273 } else {
1274 false
1275 }
1276 }) else {
1277 return Err(OperationOutcomeError::error(
1278 IssueType::NotFound(None),
1279 format!("Contained resource with id '{}' not found.", local_id),
1280 ));
1281 };
1282
1283 resource.as_ref().clone()
1284 } else {
1285 let parts = reference_string.split("/").collect::<Vec<&str>>();
1286 if parts.len() != 2 {
1287 return Err(OperationOutcomeError::error(
1288 IssueType::Invalid(None),
1289 format!("Invalid fixture reference: {}", reference_string),
1290 ));
1291 }
1292
1293 let resource_type = parts[0];
1294 let id = parts[1];
1295
1296 let Some(remote_resource) = client
1297 .read(
1298 ctx.clone(),
1299 ResourceType::try_from(resource_type).map_err(|_| {
1300 OperationOutcomeError::error(
1301 IssueType::Invalid(None),
1302 format!(
1303 "Invalid resource type in fixture reference: '{}'",
1304 resource_type
1305 ),
1306 )
1307 })?,
1308 id.to_string(),
1309 )
1310 .await?
1311 else {
1312 return Err(OperationOutcomeError::error(
1313 IssueType::NotFound(None),
1314 format!("Resource '{}' with id '{}' not found.", resource_type, id),
1315 ));
1316 };
1317
1318 remote_resource
1319 };
1320
1321 state_lock.fixtures.insert(
1322 fixture.id.clone().unwrap_or_default(),
1323 Fixtures::Resource(resolved_resource),
1324 );
1325 }
1326 }
1327
1328 drop(state_lock);
1329
1330 Ok(state)
1331}
1332
1333async fn run_setup<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1334 client: &Client,
1335 ctx: CTX,
1336 state: Arc<Mutex<TestState>>,
1337 pointer: Pointer<TestScript, TestScriptSetup>,
1338 options: Arc<TestRunnerOptions>,
1339) -> Result<TestResult<TestReportSetup>, TestScriptError> {
1340 let mut cur_state = state;
1341
1342 let mut setup_results = TestReportSetup {
1343 action: vec![],
1344 ..Default::default()
1345 };
1346
1347 let Some(setup) = pointer.value() else {
1348 return Ok(TestResult {
1349 state: cur_state,
1350 value: setup_results,
1351 });
1352 };
1353
1354 for action in setup.action.iter().enumerate() {
1355 let action_pointer = pointer
1356 .descend::<Vec<TestScriptSetupAction>>(&Key::Field("action".to_string()))
1357 .and_then(|p| p.descend::<TestScriptSetupAction>(&Key::Index(action.0)));
1358
1359 let action_pointer = action_pointer.ok_or_else(|| {
1360 TestScriptError::ExecutionError(format!(
1361 "Failed to retrieve TestScript action at index {}.",
1362 action.0
1363 ))
1364 })?;
1365
1366 let result = run_setup_action(
1367 client,
1368 ctx.clone(),
1369 cur_state,
1370 action_pointer,
1371 options.clone(),
1372 )
1373 .await?;
1374 cur_state = result.state;
1375
1376 setup_results.action.push(result.value);
1377 }
1378
1379 Ok(TestResult {
1380 state: cur_state,
1381 value: setup_results,
1382 })
1383}
1384
1385async fn run_teardown<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1386 client: &Client,
1387 ctx: CTX,
1388 state: Arc<Mutex<TestState>>,
1389 pointer: Pointer<TestScript, TestScriptTeardown>,
1390 options: Arc<TestRunnerOptions>,
1391) -> Result<TestResult<TestReportTeardown>, TestScriptError> {
1392 let mut cur_state = state;
1393
1394 let mut teardown_results = TestReportTeardown {
1395 action: vec![],
1396 ..Default::default()
1397 };
1398
1399 let Some(actions) = pointer.value() else {
1400 return Ok(TestResult {
1401 state: cur_state,
1402 value: teardown_results,
1403 });
1404 };
1405
1406 for action in actions.action.iter().enumerate() {
1407 let action_pointer = pointer
1408 .descend::<Vec<TestScriptTeardownAction>>(&Key::Field("action".to_string()))
1409 .and_then(|p| p.descend::<TestScriptTeardownAction>(&Key::Index(action.0)));
1410
1411 let action_pointer = action_pointer.ok_or_else(|| {
1412 TestScriptError::ExecutionError(format!(
1413 "Failed to retrieve TestScript teardown action at index {}.",
1414 action.0
1415 ))
1416 })?;
1417
1418 let operation_pointer = action_pointer
1419 .descend::<TestScriptSetupActionOperation>(&Key::Field("operation".to_string()))
1420 .ok_or_else(|| {
1421 TestScriptError::ExecutionError(format!(
1422 "Failed to retrieve TestScript teardown operation at index {}.",
1423 action.0
1424 ))
1425 })?;
1426
1427 let result = run_operation(
1428 client,
1429 ctx.clone(),
1430 cur_state,
1431 operation_pointer,
1432 options.clone(),
1433 )
1434 .await?;
1435 cur_state = result.state;
1436
1437 teardown_results.action.push(TestReportTeardownAction {
1438 operation: result.value,
1439 ..Default::default()
1440 });
1441 }
1442
1443 Ok(TestResult {
1444 state: cur_state,
1445 value: teardown_results,
1446 })
1447}
1448
1449async fn run_test<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1450 client: &Client,
1451 ctx: CTX,
1452 state: Arc<Mutex<TestState>>,
1453 pointer: Pointer<TestScript, TestScriptTest>,
1454 options: Arc<TestRunnerOptions>,
1455) -> Result<TestResult<TestReportTest>, TestScriptError> {
1456 let mut cur_state = state;
1457 let mut test_report_test = TestReportTest {
1458 action: vec![],
1459 ..Default::default()
1460 };
1461
1462 let test = pointer.value().ok_or_else(|| {
1463 TestScriptError::ExecutionError(format!(
1464 "Failed to retrieve TestScript test at '{}'.",
1465 pointer.path()
1466 ))
1467 })?;
1468
1469 for action in test.action.iter().enumerate() {
1470 let Some(action_pointer) = pointer
1471 .descend::<Vec<TestScriptTestAction>>(&Key::Field("action".to_string()))
1472 .and_then(|p| p.descend(&Key::Index(action.0)))
1473 else {
1474 return Err(TestScriptError::ExecutionError(format!(
1475 "Failed to retrieve TestScript test action at index {}.",
1476 action.0
1477 )));
1478 };
1479 let result = run_action(
1480 client,
1481 ctx.clone(),
1482 cur_state,
1483 action_pointer,
1484 options.clone(),
1485 )
1486 .await?;
1487 cur_state = result.state;
1488 test_report_test.action.push(TestReportTestAction {
1489 operation: result.value.operation,
1490 assert: result.value.assert,
1491 ..Default::default()
1492 });
1493 }
1494
1495 Ok(TestResult {
1496 state: cur_state,
1497 value: test_report_test,
1498 })
1499}
1500
1501async fn run_tests<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1502 client: &Client,
1503 ctx: CTX,
1504 state: Arc<Mutex<TestState>>,
1505 pointer: Pointer<TestScript, Vec<TestScriptTest>>,
1506 options: Arc<TestRunnerOptions>,
1507) -> Result<TestResult<Vec<TestReportTest>>, TestScriptError> {
1508 let mut test_results = vec![];
1509 let mut cur_state = state;
1510
1511 let Some(tests) = pointer.value() else {
1512 return Ok(TestResult {
1513 state: cur_state,
1514 value: test_results,
1515 });
1516 };
1517
1518 for test in tests.iter().enumerate() {
1519 let Some(test_pointer) = pointer.descend(&Key::Index(test.0)) else {
1520 return Err(TestScriptError::ExecutionError(format!(
1521 "Failed to retrieve TestScript test at index {}.",
1522 test.0
1523 )));
1524 };
1525 let test_result = run_test(
1526 client,
1527 ctx.clone(),
1528 cur_state,
1529 test_pointer,
1530 options.clone(),
1531 )
1532 .await?;
1533 cur_state = test_result.state;
1534 test_results.push(test_result.value);
1535 }
1536
1537 Ok(TestResult {
1538 state: cur_state,
1539 value: test_results,
1540 })
1541}
1542
1543pub struct TestRunnerOptions {
1544 pub wait_between_operations: Option<Duration>,
1545}
1546
1547pub async fn run<CTX: Clone, Client: FHIRClient<CTX, OperationOutcomeError>>(
1548 client: &Client,
1549 ctx: CTX,
1550 test_script: Arc<TestScript>,
1551 options: Arc<TestRunnerOptions>,
1552) -> Result<TestReport, TestScriptError> {
1553 tracing::info!("Running TestScript Runner with FHIR Client");
1555
1556 let mut test_report = TestReport {
1557 status: Box::new(ReportStatusCodes::Completed(None)),
1558 testScript: Box::new(Reference {
1559 reference: Some(Box::new(FHIRString {
1560 value: Some(format!(
1561 "Testscript/{}",
1562 test_script.id.clone().unwrap_or_default()
1563 )),
1564 ..Default::default()
1565 })),
1566 ..Default::default()
1567 }),
1568 ..Default::default()
1569 };
1570
1571 let mut state = Arc::new(Mutex::new(TestState::new()));
1572 let pointer = Pointer::<TestScript, TestScript>::new(test_script);
1573
1574 state = setup_fixtures(client, ctx.clone(), state, pointer.clone(), options.clone())
1575 .await
1576 .map_err(|e| TestScriptError::OperationError(e))?;
1577
1578 let mut running_state = Ok(());
1579
1580 if let Some(setup_pointer) =
1582 pointer.descend::<TestScriptSetup>(&Key::Field("setup".to_string()))
1583 {
1584 tracing::info!("Running TestScript setup...");
1585 let setup_result = run_setup(
1586 client,
1587 ctx.clone(),
1588 state.clone(),
1589 setup_pointer,
1590 options.clone(),
1591 )
1592 .await;
1593 match setup_result {
1594 Ok(res) => {
1595 state = res.state;
1596 test_report.setup = Some(res.value);
1597 }
1598 Err(e) => {
1599 running_state = Err(e);
1600 }
1601 }
1602 }
1603
1604 if running_state.is_ok()
1606 && let Some(test_pointer) =
1607 pointer.descend::<Vec<TestScriptTest>>(&Key::Field("test".to_string()))
1608 {
1609 tracing::info!("Running TestScript tests...");
1610 let test_result = run_tests(
1611 client,
1612 ctx.clone(),
1613 state.clone(),
1614 test_pointer,
1615 options.clone(),
1616 )
1617 .await;
1618
1619 match test_result {
1620 Ok(res) => {
1621 state = res.state;
1622 test_report.test = Some(res.value);
1623 }
1624
1625 Err(e) => {
1626 running_state = Err(e);
1627 }
1628 }
1629 }
1630
1631 if let Some(teardown_pointer) =
1632 pointer.descend::<TestScriptTeardown>(&Key::Field("teardown".to_string()))
1633 {
1634 tracing::info!("Running TestScript teardown...");
1635
1636 let result = run_teardown(
1637 client,
1638 ctx.clone(),
1639 state.clone(),
1640 teardown_pointer,
1641 options.clone(),
1642 )
1643 .await?;
1644
1645 test_report.teardown = Some(result.value);
1647 }
1648
1649 running_state?;
1650
1651 let state_guard = state.lock().await;
1652 match &state_guard.result {
1655 ReportResultCodes::Pending(_) => {
1656 test_report.result = Box::new(ReportResultCodes::Pass(None))
1657 }
1658 status => test_report.result = Box::new(status.clone()),
1659 }
1660
1661 Ok(test_report)
1662}