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;
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
90            for input in inputs {
91                let walker = walkdir::WalkDir::new(&input).into_iter();
92
93                for entry in walker
94                    .filter_map(|e| e.ok())
95                    .filter(|e| e.metadata().unwrap().is_file())
96                    .filter(|f| f.file_name().to_string_lossy().ends_with(".json"))
97                {
98                    let testscripts = load_testscript_files(&entry.path());
99                    for testscript in testscripts.into_iter() {
100                        let testscript = Arc::new(testscript);
101
102                        let Some(testscript_id) = testscript.id.as_ref() else {
103                            info!(
104                                "Skipping TestScript without ID from file: {}",
105                                entry.path().to_string_lossy()
106                            );
107                            continue;
108                        };
109
110                        info!(
111                            "Running TestScript '{}' from file: {}",
112                            testscript
113                                .name
114                                .value
115                                .clone()
116                                .unwrap_or("<Unnamed TestScript>".to_string()),
117                            entry.path().to_string_lossy()
118                        );
119
120                        match haste_testscript_runner::run(
121                            fhir_client.as_ref(),
122                            (),
123                            testscript.clone(),
124                            testrunner_options.clone(),
125                        )
126                        .await
127                        {
128                            Ok(mut test_report) => {
129                                test_report.id = Some(format!("report-{}", testscript_id));
130
131                                match test_report.result.as_ref() {
132                                    ReportResultCodes::Fail(_) => status_code = 1,
133                                    // Ignore for rest.
134                                    ReportResultCodes::Pass(_)
135                                    | ReportResultCodes::Pending(_)
136                                    | ReportResultCodes::Null(_) => {}
137                                }
138
139                                testreport_entries.push(BundleEntry {
140                                    request: Some(BundleEntryRequest {
141                                        method: Box::new(HttpVerb::PUT(None)),
142                                        url: Box::new(FHIRUri {
143                                            value: Some(format!("TestReport/{}", testscript_id)),
144                                            ..Default::default()
145                                        }),
146                                        ..Default::default()
147                                    }),
148                                    resource: Some(Box::new(Resource::TestReport(test_report))),
149                                    ..Default::default()
150                                });
151                            }
152                            Err(e) => {
153                                info!("Error running TestScript '{:?}'", e);
154                            }
155                        }
156                    }
157                }
158            }
159
160            let testreport_bundle = Bundle {
161                type_: Box::new(BundleType::Transaction(None)),
162                entry: Some(testreport_entries),
163                ..Default::default()
164            };
165
166            if let Some(output) = output {
167                std::fs::write(
168                    output,
169                    haste_fhir_serialization_json::to_string(&testreport_bundle).map_err(|e| {
170                        OperationOutcomeError::fatal(
171                            IssueType::Exception(None),
172                            format!("Failed to serialize TestReport bundle: {}", e),
173                        )
174                    })?,
175                )
176                .expect("Failed to write TestReport bundle to file");
177            } else {
178                println!(
179                    "{}",
180                    haste_fhir_serialization_json::to_string(&testreport_bundle).map_err(|e| {
181                        OperationOutcomeError::fatal(
182                            IssueType::Exception(None),
183                            format!("Failed to serialize TestReport bundle: {}", e),
184                        )
185                    })?
186                );
187            }
188
189            if status_code != 0 {
190                Err(OperationOutcomeError::fatal(
191                    IssueType::Exception(None),
192                    "One or more TestScripts failed".to_string(),
193                ))
194            } else {
195                Ok(())
196            }
197        }
198    }
199}