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 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                            // Ignore for rest.
151                            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}