haste_health/commands/
testscript.rs1use crate::CLIState;
2use clap::Subcommand;
3use haste_fhir_model::r4::generated::{
4 resources::{Bundle, BundleEntry, BundleEntryRequest, Resource, TestScript},
5 terminology::{BundleType, HttpVerb, IssueType, ReportResultCodes},
6 types::FHIRUri,
7};
8use haste_fhir_operation_error::OperationOutcomeError;
9use haste_testscript_runner::TestRunnerOptions;
10use std::{path::Path, sync::Arc};
11use tokio::sync::Mutex;
12use tracing::info;
13
14#[derive(Subcommand)]
15pub enum TestScriptCommands {
16 Run {
17 #[arg(short, long)]
18 input: Vec<String>,
19 #[arg(short, long)]
20 output: Option<String>,
21 #[arg(short, long)]
22 wait_between_operations_ms: Option<u64>,
23 },
24}
25
26fn load_testscript_files(path: &Path) -> Vec<TestScript> {
27 let mut testscripts = vec![];
28
29 let Ok(data) = std::fs::read_to_string(path).map_err(|e| format!("Failed to read file: {}", e))
30 else {
31 return vec![];
32 };
33
34 let resource = match haste_fhir_serialization_json::from_str::<Resource>(&data) {
35 Ok(resource) => resource,
36 Err(e) => {
37 println!(
38 "Failed to parse FHIR resource from file {}: {}",
39 path.display(),
40 e
41 );
42 return vec![];
43 }
44 };
45
46 match resource {
47 Resource::Bundle(bundle) => bundle
48 .entry
49 .unwrap_or(vec![])
50 .into_iter()
51 .for_each(|entry| {
52 if let Some(resource) = entry.resource {
53 match *resource {
54 Resource::TestScript(testscript) => {
55 testscripts.push(testscript);
56 }
57 _ => {}
58 }
59 }
60 }),
61 Resource::TestScript(testscript) => {
62 testscripts.push(testscript);
63 }
64 _ => {}
65 }
66
67 testscripts
68}
69
70pub async fn testscript_commands(
71 state: Arc<Mutex<CLIState>>,
72 command: &TestScriptCommands,
73) -> Result<(), OperationOutcomeError> {
74 match command {
75 TestScriptCommands::Run {
76 output,
77 input: inputs,
78 wait_between_operations_ms,
79 } => {
80 let fhir_client = crate::client::fhir_client(state).await?;
81
82 let mut testreport_entries = vec![];
83 let testrunner_options = Arc::new(TestRunnerOptions {
84 wait_between_operations: wait_between_operations_ms
85 .map(|ms| std::time::Duration::from_millis(ms)),
86 });
87
88 let mut status_code = 0;
89
90 for input in inputs {
91 let walker = walkdir::WalkDir::new(&input).into_iter();
92
93 for entry in walker
94 .filter_map(|e| e.ok())
95 .filter(|e| e.metadata().unwrap().is_file())
96 .filter(|f| f.file_name().to_string_lossy().ends_with(".json"))
97 {
98 let testscripts = load_testscript_files(&entry.path());
99 for testscript in testscripts.into_iter() {
100 let testscript = Arc::new(testscript);
101
102 let Some(testscript_id) = testscript.id.as_ref() else {
103 info!(
104 "Skipping TestScript without ID from file: {}",
105 entry.path().to_string_lossy()
106 );
107 continue;
108 };
109
110 info!(
111 "Running TestScript '{}' from file: {}",
112 testscript
113 .name
114 .value
115 .clone()
116 .unwrap_or("<Unnamed TestScript>".to_string()),
117 entry.path().to_string_lossy()
118 );
119
120 match haste_testscript_runner::run(
121 fhir_client.as_ref(),
122 (),
123 testscript.clone(),
124 testrunner_options.clone(),
125 )
126 .await
127 {
128 Ok(mut test_report) => {
129 test_report.id = Some(format!("report-{}", testscript_id));
130
131 match test_report.result.as_ref() {
132 ReportResultCodes::Fail(_) => status_code = 1,
133 ReportResultCodes::Pass(_)
135 | ReportResultCodes::Pending(_)
136 | ReportResultCodes::Null(_) => {}
137 }
138
139 testreport_entries.push(BundleEntry {
140 request: Some(BundleEntryRequest {
141 method: Box::new(HttpVerb::PUT(None)),
142 url: Box::new(FHIRUri {
143 value: Some(format!("TestReport/{}", testscript_id)),
144 ..Default::default()
145 }),
146 ..Default::default()
147 }),
148 resource: Some(Box::new(Resource::TestReport(test_report))),
149 ..Default::default()
150 });
151 }
152 Err(e) => {
153 info!("Error running TestScript '{:?}'", e);
154 }
155 }
156 }
157 }
158 }
159
160 let testreport_bundle = Bundle {
161 type_: Box::new(BundleType::Transaction(None)),
162 entry: Some(testreport_entries),
163 ..Default::default()
164 };
165
166 if let Some(output) = output {
167 std::fs::write(
168 output,
169 haste_fhir_serialization_json::to_string(&testreport_bundle).map_err(|e| {
170 OperationOutcomeError::fatal(
171 IssueType::Exception(None),
172 format!("Failed to serialize TestReport bundle: {}", e),
173 )
174 })?,
175 )
176 .expect("Failed to write TestReport bundle to file");
177 } else {
178 println!(
179 "{}",
180 haste_fhir_serialization_json::to_string(&testreport_bundle).map_err(|e| {
181 OperationOutcomeError::fatal(
182 IssueType::Exception(None),
183 format!("Failed to serialize TestReport bundle: {}", e),
184 )
185 })?
186 );
187 }
188
189 if status_code != 0 {
190 Err(OperationOutcomeError::fatal(
191 IssueType::Exception(None),
192 "One or more TestScripts failed".to_string(),
193 ))
194 } else {
195 Ok(())
196 }
197 }
198 }
199}