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 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 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 let testscripts = load_testscript_files(&entry.path());
100 for testscript in testscripts.into_iter() {
101 let testscript = Arc::new(testscript);
102
103 let Some(testscript_id) = testscript.id.as_ref() else {
104 info!(
105 "Skipping TestScript without ID from file: {}",
106 entry.path().to_string_lossy()
107 );
108 continue;
109 };
110
111 info!(
112 "Running TestScript '{}' from file: {}",
113 testscript
114 .name
115 .value
116 .clone()
117 .unwrap_or("<Unnamed TestScript>".to_string()),
118 entry.path().to_string_lossy()
119 );
120
121 let testscript_id = testscript_id.clone();
122 let testrunner_options = testrunner_options.clone();
123 let fhir_client = fhir_client.clone();
124
125 test_runs.spawn(async move {
126 match haste_testscript_runner::run(
127 fhir_client.as_ref(),
128 (),
129 testscript,
130 testrunner_options,
131 )
132 .await
133 {
134 Ok(mut test_report) => {
135 test_report.id = Some(testscript_id);
136 Ok(test_report)
137 }
138 Err(e) => Err(e),
139 }
140 });
141 }
142 }
143 }
144
145 while let Some(Ok(res)) = test_runs.join_next().await {
146 match res {
147 Ok(test_report) => {
148 match test_report.result.as_ref() {
149 ReportResultCodes::Fail(_) => status_code = 1,
150 ReportResultCodes::Pass(_)
152 | ReportResultCodes::Pending(_)
153 | ReportResultCodes::Null(_) => {}
154 }
155
156 testreport_entries.push(BundleEntry {
157 request: Some(BundleEntryRequest {
158 method: Box::new(HttpVerb::PUT(None)),
159 url: Box::new(FHIRUri {
160 value: Some(format!(
161 "TestReport/{}",
162 test_report.id.as_ref().map(|id| id.as_str()).unwrap_or("")
163 )),
164 ..Default::default()
165 }),
166 ..Default::default()
167 }),
168 resource: Some(Box::new(Resource::TestReport(test_report))),
169 ..Default::default()
170 });
171 }
172 Err(e) => {
173 info!("Error running TestScript '{:?}'", e);
174 }
175 }
176 }
177
178 let testreport_bundle = Bundle {
179 type_: Box::new(BundleType::Transaction(None)),
180 entry: Some(testreport_entries),
181 ..Default::default()
182 };
183
184 if let Some(output) = output {
185 std::fs::write(
186 output,
187 haste_fhir_serialization_json::to_string(&testreport_bundle).map_err(|e| {
188 OperationOutcomeError::fatal(
189 IssueType::Exception(None),
190 format!("Failed to serialize TestReport bundle: {}", e),
191 )
192 })?,
193 )
194 .expect("Failed to write TestReport bundle to file");
195 } else {
196 println!(
197 "{}",
198 haste_fhir_serialization_json::to_string(&testreport_bundle).map_err(|e| {
199 OperationOutcomeError::fatal(
200 IssueType::Exception(None),
201 format!("Failed to serialize TestReport bundle: {}", e),
202 )
203 })?
204 );
205 }
206
207 if status_code != 0 {
208 Err(OperationOutcomeError::fatal(
209 IssueType::Exception(None),
210 "One or more TestScripts failed".to_string(),
211 ))
212 } else {
213 Ok(())
214 }
215 }
216 }
217}