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::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 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 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.as_ref() {
150 ReportResultCodes::Fail(_) => status_code = 1,
151 ReportResultCodes::Pass(_)
153 | ReportResultCodes::Pending(_)
154 | ReportResultCodes::Null(_) => {}
155 }
156
157 testreport_entries.push(BundleEntry {
158 request: Some(BundleEntryRequest {
159 method: Box::new(HttpVerb::PUT(None)),
160 url: Box::new(FHIRUri {
161 value: Some(format!(
162 "TestReport/{}",
163 test_report.id.as_ref().map(|id| id.as_str()).unwrap_or("")
164 )),
165 ..Default::default()
166 }),
167 ..Default::default()
168 }),
169 resource: Some(Box::new(Resource::TestReport(test_report))),
170 ..Default::default()
171 });
172 }
173 Err(e) => {
174 info!("Error running TestScript '{:?}'", e);
175 }
176 }
177 }
178
179 let testreport_bundle = Bundle {
180 type_: Box::new(BundleType::Transaction(None)),
181 entry: Some(testreport_entries),
182 ..Default::default()
183 };
184
185 if let Some(output) = output {
186 std::fs::write(
187 output,
188 serde_json::to_string(&testreport_bundle).map_err(|e| {
189 OperationOutcomeError::fatal(
190 IssueType::Exception(None),
191 format!("Failed to serialize TestReport bundle: {}", e),
192 )
193 })?,
194 )
195 .expect("Failed to write TestReport bundle to file");
196 } else {
197 println!(
198 "{}",
199 serde_json::to_string(&testreport_bundle).map_err(|e| {
200 OperationOutcomeError::fatal(
201 IssueType::Exception(None),
202 format!("Failed to serialize TestReport bundle: {}", e),
203 )
204 })?
205 );
206 }
207
208 if status_code != 0 {
209 Err(OperationOutcomeError::fatal(
210 IssueType::Exception(None),
211 "One or more TestScripts failed".to_string(),
212 ))
213 } else {
214 Ok(())
215 }
216 }
217 }
218}