1use std::path::{Path, PathBuf};
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use serde::{Deserialize, Serialize};
5
6use crate::{
7 synthetic_source::{deterministic_rgb_pixels, write_rgb_source_dicom},
8 validate_dicom_path, Error, Export, ExportOptions, ExportReport, MetadataSource,
9 ValidationOptions, ValidationReport,
10};
11
12#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(default)]
15#[non_exhaustive]
16pub struct SelfTestOptions {
17 pub output_dir: Option<PathBuf>,
19 pub keep_output: bool,
21 pub export: ExportOptions,
23 pub validation: ValidationOptions,
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize)]
29#[non_exhaustive]
30pub struct SelfTestReport {
31 pub workspace: PathBuf,
33 pub source_path: PathBuf,
35 pub output_dir: PathBuf,
37 pub kept_output: bool,
39 pub export_report: ExportReport,
41 pub validation_report: ValidationReport,
43}
44
45pub fn run_dicom_self_test(options: SelfTestOptions) -> Result<SelfTestReport, Error> {
47 let workspace = SelfTestWorkspace::create(options.output_dir.as_deref(), options.keep_output)?;
48 let source_path = workspace.path().join("source.dcm");
49 let output_dir = workspace.path().join("dicom");
50 std::fs::create_dir_all(&output_dir).map_err(|source| Error::Io {
51 path: output_dir.clone(),
52 source,
53 })?;
54 write_self_test_source_dicom(&source_path)?;
55
56 let export_options = options.export.clone();
57 export_options.validate()?;
58 let export_report = Export::from_slide(&source_path)
59 .to_directory(&output_dir)
60 .with_metadata(MetadataSource::ResearchPlaceholder)
61 .with_options(export_options)
62 .run()?;
63 let validation_report = validate_dicom_path(&output_dir, &options.validation)?;
64 let kept_output = workspace.kept_output();
65 let workspace_path = workspace.path().to_path_buf();
66 if kept_output {
67 workspace.keep();
68 }
69
70 Ok(SelfTestReport {
71 workspace: workspace_path,
72 source_path,
73 output_dir,
74 kept_output,
75 export_report,
76 validation_report,
77 })
78}
79
80struct SelfTestWorkspace {
81 path: PathBuf,
82 cleanup: bool,
83}
84
85impl SelfTestWorkspace {
86 fn create(output_dir: Option<&Path>, keep_output: bool) -> Result<Self, Error> {
87 if let Some(path) = output_dir {
88 std::fs::create_dir_all(path).map_err(|source| Error::Io {
89 path: path.to_path_buf(),
90 source,
91 })?;
92 return Ok(Self {
93 path: path.to_path_buf(),
94 cleanup: false,
95 });
96 }
97
98 let base = std::env::temp_dir();
99 let nanos = SystemTime::now()
100 .duration_since(UNIX_EPOCH)
101 .map(|duration| duration.as_nanos())
102 .unwrap_or(0);
103 for attempt in 0..1000u32 {
104 let path = base.join(format!(
105 "wsi-dicom-self-test-{}-{nanos}-{attempt}",
106 std::process::id()
107 ));
108 match std::fs::create_dir(&path) {
109 Ok(()) => {
110 return Ok(Self {
111 path,
112 cleanup: !keep_output,
113 });
114 }
115 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
116 Err(source) => return Err(Error::Io { path, source }),
117 }
118 }
119
120 Err(Error::Validation {
121 reason: "failed to create a unique self-test directory".into(),
122 })
123 }
124
125 fn path(&self) -> &Path {
126 &self.path
127 }
128
129 fn kept_output(&self) -> bool {
130 !self.cleanup
131 }
132
133 fn keep(mut self) {
134 self.cleanup = false;
135 }
136}
137
138impl Drop for SelfTestWorkspace {
139 fn drop(&mut self) {
140 if self.cleanup {
141 if let Err(err) = std::fs::remove_dir_all(&self.path) {
142 if err.kind() != std::io::ErrorKind::NotFound {
143 eprintln!(
144 "wsi-dicom: failed to remove self-test workspace {}: {err}",
145 self.path.display()
146 );
147 }
148 }
149 }
150 }
151}
152
153fn write_self_test_source_dicom(path: &Path) -> Result<(), Error> {
154 let width = 4u32;
155 let height = 4u32;
156 let sop_instance_uid = "1.2.826.0.1.3680043.10.999.2000";
157 write_rgb_source_dicom(
158 path,
159 sop_instance_uid,
160 "1.2.826.0.1.3680043.10.999.200",
161 width,
162 height,
163 deterministic_rgb_pixels(width, height),
164 )
165}
166
167#[cfg(test)]
168mod tests {
169 use super::{run_dicom_self_test, SelfTestOptions};
170 use crate::ValidationOptions;
171
172 #[test]
173 fn self_test_writes_output_and_validation_report_when_output_is_kept() {
174 let tmp = tempfile::tempdir().expect("tempdir");
175 let workspace = tmp.path().join("evidence");
176
177 let report = run_dicom_self_test(SelfTestOptions {
178 output_dir: Some(workspace.clone()),
179 keep_output: true,
180 validation: ValidationOptions {
181 max_pixel_frames: 0,
182 ..ValidationOptions::default()
183 },
184 ..SelfTestOptions::default()
185 })
186 .expect("self-test report");
187
188 assert!(report.kept_output);
189 assert_eq!(report.workspace, workspace);
190 assert!(report.output_dir.is_dir());
191 assert!(!report.export_report.instances.is_empty());
192 assert_eq!(report.validation_report.failed_checks(), 0);
193 }
194}