1use std::fs;
2use std::path::{Path, PathBuf};
3
4use serde::Serialize;
5use serde_json::Value;
6
7use crate::probe::{ProbeObservation, ViewerProbe};
8use crate::results::sha256_file;
9use crate::shim::{FixtureSet, ReferenceShim};
10
11mod expected;
12mod geojson;
13mod inputs;
14mod observation;
15mod parametric_map;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
18pub enum ConversionMatrixKind {
19 #[serde(rename = "ann-seg")]
20 AnnSeg,
21 #[serde(rename = "sr")]
22 Sr,
23 #[serde(rename = "pm")]
24 Pm,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
28#[serde(rename_all = "lowercase")]
29pub enum ConversionTarget {
30 Ann,
31 Seg,
32 Sr,
33 Pm,
34}
35
36impl ConversionTarget {
37 const fn matrix(self) -> ConversionMatrixKind {
38 match self {
39 Self::Ann | Self::Seg => ConversionMatrixKind::AnnSeg,
40 Self::Sr => ConversionMatrixKind::Sr,
41 Self::Pm => ConversionMatrixKind::Pm,
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
47#[serde(rename_all = "lowercase")]
48pub enum ConversionStatus {
49 Passed,
50 Failed,
51}
52
53impl ConversionStatus {
54 #[must_use]
55 pub const fn is_passed(self) -> bool {
56 matches!(self, Self::Passed)
57 }
58}
59
60#[derive(Debug, Serialize)]
61pub struct ConversionObservation {
62 pub matrix: ConversionMatrixKind,
63 pub case_id: String,
64 pub target: ConversionTarget,
65 pub status: ConversionStatus,
66 pub highdicom_readable: bool,
67 pub output_paths: Vec<PathBuf>,
68 pub report: Value,
69 pub normalized: Value,
70 pub command: Vec<String>,
71 pub runtime_ms: f64,
73 pub peak_rss_bytes: u64,
74 pub peak_tracked_heap_bytes: u64,
75 pub message: String,
76}
77
78#[derive(Debug, Serialize)]
79pub struct ConversionMatrixResult {
80 pub observations: Vec<ConversionObservation>,
81}
82
83impl ConversionMatrixResult {
84 #[must_use]
85 pub fn is_ok(&self) -> bool {
86 !self.observations.is_empty()
87 && self
88 .observations
89 .iter()
90 .all(|observation| observation.status.is_passed())
91 }
92}
93
94pub fn run_conversion_matrices(
101 fixtures: &FixtureSet,
102 reference: &ReferenceShim,
103 probe: &ViewerProbe,
104 output_directory: &Path,
105) -> Result<ConversionMatrixResult, String> {
106 fs::create_dir(output_directory).map_err(|error| {
107 format!(
108 "could not create conversion matrix directory {}: {error}",
109 output_directory.display()
110 )
111 })?;
112 let inputs = inputs::prepare(&output_directory.join("inputs"))?;
113 let mut observations = Vec::with_capacity(6);
114 observations.extend(geojson::run_direct(
115 fixtures,
116 reference,
117 probe,
118 output_directory,
119 &inputs,
120 ));
121 observations.extend(geojson::run_seg_reference(
122 fixtures,
123 reference,
124 probe,
125 output_directory,
126 &inputs,
127 ));
128 observations.extend(parametric_map::run_single(
129 fixtures,
130 reference,
131 probe,
132 output_directory,
133 &inputs,
134 ));
135 observations.extend(parametric_map::run_concatenation(
136 fixtures,
137 reference,
138 probe,
139 output_directory,
140 &inputs,
141 ));
142 Ok(ConversionMatrixResult { observations })
143}
144
145pub(super) fn verify_report_outputs(
146 observation: &ProbeObservation,
147 target: &str,
148 expected_paths: &[PathBuf],
149) -> Result<(), String> {
150 let outputs = observation.report["outputs"]
151 .as_array()
152 .ok_or_else(|| "conversion report outputs must be an array".to_owned())?
153 .iter()
154 .filter(|output| output["target"].as_str() == Some(target))
155 .collect::<Vec<_>>();
156 if outputs.len() != expected_paths.len() {
157 return Err(format!(
158 "conversion report listed {} {target} outputs; expected {}",
159 outputs.len(),
160 expected_paths.len()
161 ));
162 }
163 for (output, path) in outputs.into_iter().zip(expected_paths) {
164 if !path.is_file() {
165 return Err(format!("conversion output is missing: {}", path.display()));
166 }
167 let canonical = path
168 .canonicalize()
169 .map_err(|error| format!("could not canonicalize {}: {error}", path.display()))?;
170 if output["path"].as_str() != Some(canonical.to_string_lossy().as_ref()) {
171 return Err(format!(
172 "conversion report path does not match {}",
173 canonical.display()
174 ));
175 }
176 let bytes = fs::metadata(path)
177 .map_err(|error| format!("could not stat {}: {error}", path.display()))?
178 .len();
179 if output["bytes"].as_u64() != Some(bytes) {
180 return Err(format!(
181 "conversion report byte count differs for {}",
182 path.display()
183 ));
184 }
185 let checksum = sha256_file(path).map_err(|error| error.to_string())?;
186 if output["sha256"].as_str() != Some(checksum.as_str()) {
187 return Err(format!(
188 "conversion report checksum differs for {}",
189 path.display()
190 ));
191 }
192 }
193 Ok(())
194}