jugar_probar/pixel_coverage/
config.rs1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(default)]
11pub struct PixelCoverageConfig {
12 pub enabled: bool,
14 pub methodology: String,
16 pub thresholds: ThresholdConfig,
18 pub verification: VerificationConfig,
20 pub output: OutputConfig,
22 pub performance: PerformanceConfig,
24}
25
26impl Default for PixelCoverageConfig {
27 fn default() -> Self {
28 Self {
29 enabled: true,
30 methodology: "falsification".to_string(),
31 thresholds: ThresholdConfig::default(),
32 verification: VerificationConfig::default(),
33 output: OutputConfig::default(),
34 performance: PerformanceConfig::default(),
35 }
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(default)]
42pub struct ThresholdConfig {
43 pub min_coverage: f32,
45 pub max_gap_size: f32,
47 pub falsifiability_threshold: f32,
49}
50
51impl Default for ThresholdConfig {
52 fn default() -> Self {
53 Self {
54 min_coverage: 85.0,
55 max_gap_size: 5.0,
56 falsifiability_threshold: 15.0,
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(default)]
64pub struct VerificationConfig {
65 pub ssim_threshold: f32,
67 pub delta_e_threshold: f32,
69 pub phash_distance: u32,
71 pub psnr_threshold: f32,
73}
74
75impl Default for VerificationConfig {
76 fn default() -> Self {
77 Self {
78 ssim_threshold: 0.99,
79 delta_e_threshold: 1.0,
80 phash_distance: 5,
81 psnr_threshold: 40.0,
82 }
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88#[serde(default)]
89pub struct OutputConfig {
90 pub heatmap: bool,
92 pub terminal_gui: bool,
94 pub palette: String,
96 pub highlight_gaps: bool,
98 pub show_legend: bool,
100 pub show_confidence: bool,
102}
103
104impl Default for OutputConfig {
105 fn default() -> Self {
106 Self {
107 heatmap: true,
108 terminal_gui: true,
109 palette: "viridis".to_string(),
110 highlight_gaps: true,
111 show_legend: true,
112 show_confidence: true,
113 }
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(default)]
120pub struct PerformanceConfig {
121 pub parallel: bool,
123 pub threads: usize,
125 pub enable_downscaling: bool,
127 pub downscale_factor: u32,
129 pub cache_hashes: bool,
131}
132
133impl Default for PerformanceConfig {
134 fn default() -> Self {
135 Self {
136 parallel: true,
137 threads: 0, enable_downscaling: true,
139 downscale_factor: 2,
140 cache_hashes: true,
141 }
142 }
143}
144
145impl PixelCoverageConfig {
146 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
151 serde_json::from_str(json)
152 }
153
154 pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml_ng::Error> {
159 serde_yaml_ng::from_str(yaml)
160 }
161
162 pub fn to_json(&self) -> Result<String, serde_json::Error> {
167 serde_json::to_string_pretty(self)
168 }
169
170 pub fn to_yaml(&self) -> Result<String, serde_yaml_ng::Error> {
175 serde_yaml_ng::to_string(self)
176 }
177
178 #[must_use]
180 pub fn validate(&self) -> Vec<ConfigValidationError> {
181 let mut errors = Vec::new();
182
183 if !(0.0..=100.0).contains(&self.thresholds.min_coverage) {
185 errors.push(ConfigValidationError {
186 field: "thresholds.min_coverage".to_string(),
187 message: "Must be between 0 and 100".to_string(),
188 });
189 }
190
191 if !(0.0..=100.0).contains(&self.thresholds.max_gap_size) {
192 errors.push(ConfigValidationError {
193 field: "thresholds.max_gap_size".to_string(),
194 message: "Must be between 0 and 100".to_string(),
195 });
196 }
197
198 if !(0.0..=25.0).contains(&self.thresholds.falsifiability_threshold) {
199 errors.push(ConfigValidationError {
200 field: "thresholds.falsifiability_threshold".to_string(),
201 message: "Must be between 0 and 25".to_string(),
202 });
203 }
204
205 if !(0.0..=1.0).contains(&self.verification.ssim_threshold) {
207 errors.push(ConfigValidationError {
208 field: "verification.ssim_threshold".to_string(),
209 message: "Must be between 0 and 1".to_string(),
210 });
211 }
212
213 if self.verification.delta_e_threshold < 0.0 {
214 errors.push(ConfigValidationError {
215 field: "verification.delta_e_threshold".to_string(),
216 message: "Must be non-negative".to_string(),
217 });
218 }
219
220 let valid_palettes = ["viridis", "magma", "heat"];
222 if !valid_palettes.contains(&self.output.palette.as_str()) {
223 errors.push(ConfigValidationError {
224 field: "output.palette".to_string(),
225 message: format!("Must be one of: {}", valid_palettes.join(", ")),
226 });
227 }
228
229 if self.performance.downscale_factor == 0 {
231 errors.push(ConfigValidationError {
232 field: "performance.downscale_factor".to_string(),
233 message: "Must be at least 1".to_string(),
234 });
235 }
236
237 errors
238 }
239
240 #[must_use]
242 pub fn is_valid(&self) -> bool {
243 self.validate().is_empty()
244 }
245
246 #[must_use]
248 pub fn normalized_min_coverage(&self) -> f32 {
249 self.thresholds.min_coverage / 100.0
250 }
251
252 #[must_use]
254 pub fn normalized_max_gap(&self) -> f32 {
255 self.thresholds.max_gap_size / 100.0
256 }
257}
258
259#[derive(Debug, Clone)]
261pub struct ConfigValidationError {
262 pub field: String,
264 pub message: String,
266}
267
268impl std::fmt::Display for ConfigValidationError {
269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270 write!(f, "{}: {}", self.field, self.message)
271 }
272}
273
274#[cfg(test)]
275#[allow(clippy::unwrap_used)]
276mod tests {
277 use super::*;
278
279 #[test]
284 fn h0_config_01_default() {
285 let config = PixelCoverageConfig::default();
286 assert!(config.enabled);
287 assert_eq!(config.methodology, "falsification");
288 assert!((config.thresholds.min_coverage - 85.0).abs() < f32::EPSILON);
289 }
290
291 #[test]
292 fn h0_config_02_json_roundtrip() {
293 let config = PixelCoverageConfig::default();
294 let json = config.to_json().unwrap();
295 let parsed = PixelCoverageConfig::from_json(&json).unwrap();
296 assert!((parsed.thresholds.min_coverage - 85.0).abs() < f32::EPSILON);
297 }
298
299 #[test]
300 fn h0_config_03_yaml_roundtrip() {
301 let config = PixelCoverageConfig::default();
302 let yaml = config.to_yaml().unwrap();
303 let parsed = PixelCoverageConfig::from_yaml(&yaml).unwrap();
304 assert!((parsed.thresholds.min_coverage - 85.0).abs() < f32::EPSILON);
305 }
306
307 #[test]
308 fn h0_config_04_validation_pass() {
309 let config = PixelCoverageConfig::default();
310 assert!(config.is_valid());
311 assert!(config.validate().is_empty());
312 }
313
314 #[test]
315 fn h0_config_05_validation_fail() {
316 let mut config = PixelCoverageConfig::default();
317 config.thresholds.min_coverage = 150.0; assert!(!config.is_valid());
319 assert!(!config.validate().is_empty());
320 }
321
322 #[test]
323 fn h0_config_06_validation_ssim() {
324 let mut config = PixelCoverageConfig::default();
325 config.verification.ssim_threshold = 1.5; let errors = config.validate();
327 assert!(errors.iter().any(|e| e.field.contains("ssim")));
328 }
329
330 #[test]
331 fn h0_config_07_validation_palette() {
332 let mut config = PixelCoverageConfig::default();
333 config.output.palette = "invalid".to_string();
334 let errors = config.validate();
335 assert!(errors.iter().any(|e| e.field.contains("palette")));
336 }
337
338 #[test]
339 fn h0_config_08_normalized() {
340 let config = PixelCoverageConfig::default();
341 assert!((config.normalized_min_coverage() - 0.85).abs() < f32::EPSILON);
342 assert!((config.normalized_max_gap() - 0.05).abs() < f32::EPSILON);
343 }
344
345 #[test]
346 fn h0_config_09_threshold_defaults() {
347 let threshold = ThresholdConfig::default();
348 assert!((threshold.falsifiability_threshold - 15.0).abs() < f32::EPSILON);
349 }
350
351 #[test]
352 fn h0_config_10_performance_defaults() {
353 let perf = PerformanceConfig::default();
354 assert!(perf.parallel);
355 assert_eq!(perf.threads, 0);
356 assert!(perf.enable_downscaling);
357 }
358
359 #[test]
360 fn h0_config_11_output_defaults() {
361 let output = OutputConfig::default();
362 assert!(output.heatmap);
363 assert!(output.terminal_gui);
364 assert_eq!(output.palette, "viridis");
365 }
366
367 #[test]
368 fn h0_config_12_verification_defaults() {
369 let verify = VerificationConfig::default();
370 assert!((verify.ssim_threshold - 0.99).abs() < f32::EPSILON);
371 assert_eq!(verify.phash_distance, 5);
372 }
373
374 #[test]
375 fn h0_config_13_downscale_validation() {
376 let mut config = PixelCoverageConfig::default();
377 config.performance.downscale_factor = 0; let errors = config.validate();
379 assert!(errors.iter().any(|e| e.field.contains("downscale")));
380 }
381
382 #[test]
383 fn h0_config_14_error_display() {
384 let error = ConfigValidationError {
385 field: "test.field".to_string(),
386 message: "test message".to_string(),
387 };
388 let display = format!("{}", error);
389 assert!(display.contains("test.field"));
390 assert!(display.contains("test message"));
391 }
392}