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, task::JoinSet};
12use tracing::{error, info};
13
14#[derive(Subcommand)]
15pub(crate) 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 serde_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(crate) 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 let mut test_runs = JoinSet::new();
90
91 for input in inputs {
92 let walker = walkdir::WalkDir::new(&input).into_iter();
93
94 for entry in walker
95 .filter_map(|e| e.ok())
96 .filter(|e| e.metadata().unwrap().is_file())
97 .filter(|f| f.file_name().to_string_lossy().ends_with(".json"))
98 {
99 println!("Processing file: {}", entry.path().display());
100 let testscripts = load_testscript_files(&entry.path());
101 for testscript in testscripts.into_iter() {
102 let testscript = Arc::new(testscript);
103
104 let Some(testscript_id) = testscript.id.as_ref() else {
105 info!(
106 "Skipping TestScript without ID from file: {}",
107 entry.path().to_string_lossy()
108 );
109 continue;
110 };
111
112 info!(
113 "Running TestScript '{}' from file: {}",
114 testscript
115 .name
116 .value
117 .clone()
118 .unwrap_or("<Unnamed TestScript>".to_string()),
119 entry.path().to_string_lossy()
120 );
121
122 let testscript_id = testscript_id.clone();
123 let testrunner_options = testrunner_options.clone();
124 let fhir_client = fhir_client.clone();
125
126 test_runs.spawn(async move {
127 match haste_testscript_runner::run(
128 fhir_client.as_ref(),
129 (),
130 testscript,
131 testrunner_options,
132 )
133 .await
134 {
135 Ok(mut test_report) => {
136 test_report.id = Some(testscript_id);
137 Ok(test_report)
138 }
139 Err(e) => Err(e),
140 }
141 });
142 }
143 }
144 }
145
146 while let Some(Ok(res)) = test_runs.join_next().await {
147 match res {
148 Ok(test_report) => {
149 match &test_report.result {
150 r if r == &ReportResultCodes::pass()
152 || r == &ReportResultCodes::pending()
153 || r == &ReportResultCodes::null() => {}
154 r if r == &ReportResultCodes::fail() => status_code = 1,
155 _ => status_code = 1,
156 }
157
158 testreport_entries.push(BundleEntry {
159 request: Some(BundleEntryRequest {
160 method: HttpVerb::put(),
161 url: Box::new(FHIRUri {
162 value: Some(format!(
163 "TestReport/{}",
164 test_report.id.as_ref().map(|id| id.as_str()).unwrap_or("")
165 )),
166 ..Default::default()
167 }),
168 ..Default::default()
169 }),
170 resource: Some(Box::new(Resource::TestReport(test_report))),
171 ..Default::default()
172 });
173 }
174 Err(e) => {
175 status_code = 1;
176 error!("Error running TestScript '{:?}'", e);
177 }
178 }
179 }
180
181 let testreport_bundle = Bundle {
182 type_: BundleType::transaction(),
183 entry: Some(testreport_entries),
184 ..Default::default()
185 };
186
187 if let Some(output) = output {
188 std::fs::write(
189 output,
190 serde_json::to_string(&testreport_bundle).map_err(|e| {
191 OperationOutcomeError::fatal(
192 IssueType::exception(),
193 format!("Failed to serialize TestReport bundle: {}", e),
194 )
195 })?,
196 )
197 .expect("Failed to write TestReport bundle to file");
198 } else {
199 println!(
200 "{}",
201 serde_json::to_string(&testreport_bundle).map_err(|e| {
202 OperationOutcomeError::fatal(
203 IssueType::exception(),
204 format!("Failed to serialize TestReport bundle: {}", e),
205 )
206 })?
207 );
208 }
209
210 if status_code != 0 {
211 Err(OperationOutcomeError::fatal(
212 IssueType::exception(),
213 "One or more TestScripts failed".to_string(),
214 ))
215 } else {
216 Ok(())
217 }
218 }
219 }
220}