Skip to main content

wsi_dicom/
diagnostics.rs

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/// Options for generating and validating a deterministic tiny DICOM export.
13#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(default)]
15#[non_exhaustive]
16pub struct SelfTestOptions {
17    /// Optional workspace directory for self-test inputs, outputs, and evidence.
18    pub output_dir: Option<PathBuf>,
19    /// Keep the self-test workspace after the run.
20    pub keep_output: bool,
21    /// Export options used for the generated source.
22    pub export: ExportOptions,
23    /// Validation options used against the generated output.
24    pub validation: ValidationOptions,
25}
26
27/// Report returned by the deterministic DICOM self-test.
28#[derive(Debug, Clone, PartialEq, Serialize)]
29#[non_exhaustive]
30pub struct SelfTestReport {
31    /// Workspace containing generated source and output evidence.
32    pub workspace: PathBuf,
33    /// Generated source DICOM path.
34    pub source_path: PathBuf,
35    /// Generated output DICOM directory.
36    pub output_dir: PathBuf,
37    /// Whether output evidence was preserved on disk.
38    pub kept_output: bool,
39    /// Export report from converting the generated source.
40    pub export_report: ExportReport,
41    /// Validation report for the generated output.
42    pub validation_report: ValidationReport,
43}
44
45/// Generate a tiny deterministic source DICOM, export it, and validate the output.
46pub 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}