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