Skip to main content

haste_health/commands/
testscript.rs

1use 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                            // Ignore for rest.
152                            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}