use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct PixelCoverageConfig {
pub enabled: bool,
pub methodology: String,
pub thresholds: ThresholdConfig,
pub verification: VerificationConfig,
pub output: OutputConfig,
pub performance: PerformanceConfig,
}
impl Default for PixelCoverageConfig {
fn default() -> Self {
Self {
enabled: true,
methodology: "falsification".to_string(),
thresholds: ThresholdConfig::default(),
verification: VerificationConfig::default(),
output: OutputConfig::default(),
performance: PerformanceConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ThresholdConfig {
pub min_coverage: f32,
pub max_gap_size: f32,
pub falsifiability_threshold: f32,
}
impl Default for ThresholdConfig {
fn default() -> Self {
Self {
min_coverage: 85.0,
max_gap_size: 5.0,
falsifiability_threshold: 15.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct VerificationConfig {
pub ssim_threshold: f32,
pub delta_e_threshold: f32,
pub phash_distance: u32,
pub psnr_threshold: f32,
}
impl Default for VerificationConfig {
fn default() -> Self {
Self {
ssim_threshold: 0.99,
delta_e_threshold: 1.0,
phash_distance: 5,
psnr_threshold: 40.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct OutputConfig {
pub heatmap: bool,
pub terminal_gui: bool,
pub palette: String,
pub highlight_gaps: bool,
pub show_legend: bool,
pub show_confidence: bool,
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
heatmap: true,
terminal_gui: true,
palette: "viridis".to_string(),
highlight_gaps: true,
show_legend: true,
show_confidence: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct PerformanceConfig {
pub parallel: bool,
pub threads: usize,
pub enable_downscaling: bool,
pub downscale_factor: u32,
pub cache_hashes: bool,
}
impl Default for PerformanceConfig {
fn default() -> Self {
Self {
parallel: true,
threads: 0, enable_downscaling: true,
downscale_factor: 2,
cache_hashes: true,
}
}
}
impl PixelCoverageConfig {
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml_ng::Error> {
serde_yaml_ng::from_str(yaml)
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn to_yaml(&self) -> Result<String, serde_yaml_ng::Error> {
serde_yaml_ng::to_string(self)
}
#[must_use]
pub fn validate(&self) -> Vec<ConfigValidationError> {
let mut errors = Vec::new();
if !(0.0..=100.0).contains(&self.thresholds.min_coverage) {
errors.push(ConfigValidationError {
field: "thresholds.min_coverage".to_string(),
message: "Must be between 0 and 100".to_string(),
});
}
if !(0.0..=100.0).contains(&self.thresholds.max_gap_size) {
errors.push(ConfigValidationError {
field: "thresholds.max_gap_size".to_string(),
message: "Must be between 0 and 100".to_string(),
});
}
if !(0.0..=25.0).contains(&self.thresholds.falsifiability_threshold) {
errors.push(ConfigValidationError {
field: "thresholds.falsifiability_threshold".to_string(),
message: "Must be between 0 and 25".to_string(),
});
}
if !(0.0..=1.0).contains(&self.verification.ssim_threshold) {
errors.push(ConfigValidationError {
field: "verification.ssim_threshold".to_string(),
message: "Must be between 0 and 1".to_string(),
});
}
if self.verification.delta_e_threshold < 0.0 {
errors.push(ConfigValidationError {
field: "verification.delta_e_threshold".to_string(),
message: "Must be non-negative".to_string(),
});
}
let valid_palettes = ["viridis", "magma", "heat"];
if !valid_palettes.contains(&self.output.palette.as_str()) {
errors.push(ConfigValidationError {
field: "output.palette".to_string(),
message: format!("Must be one of: {}", valid_palettes.join(", ")),
});
}
if self.performance.downscale_factor == 0 {
errors.push(ConfigValidationError {
field: "performance.downscale_factor".to_string(),
message: "Must be at least 1".to_string(),
});
}
errors
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.validate().is_empty()
}
#[must_use]
pub fn normalized_min_coverage(&self) -> f32 {
self.thresholds.min_coverage / 100.0
}
#[must_use]
pub fn normalized_max_gap(&self) -> f32 {
self.thresholds.max_gap_size / 100.0
}
}
#[derive(Debug, Clone)]
pub struct ConfigValidationError {
pub field: String,
pub message: String,
}
impl std::fmt::Display for ConfigValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.field, self.message)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn h0_config_01_default() {
let config = PixelCoverageConfig::default();
assert!(config.enabled);
assert_eq!(config.methodology, "falsification");
assert!((config.thresholds.min_coverage - 85.0).abs() < f32::EPSILON);
}
#[test]
fn h0_config_02_json_roundtrip() {
let config = PixelCoverageConfig::default();
let json = config.to_json().unwrap();
let parsed = PixelCoverageConfig::from_json(&json).unwrap();
assert!((parsed.thresholds.min_coverage - 85.0).abs() < f32::EPSILON);
}
#[test]
fn h0_config_03_yaml_roundtrip() {
let config = PixelCoverageConfig::default();
let yaml = config.to_yaml().unwrap();
let parsed = PixelCoverageConfig::from_yaml(&yaml).unwrap();
assert!((parsed.thresholds.min_coverage - 85.0).abs() < f32::EPSILON);
}
#[test]
fn h0_config_04_validation_pass() {
let config = PixelCoverageConfig::default();
assert!(config.is_valid());
assert!(config.validate().is_empty());
}
#[test]
fn h0_config_05_validation_fail() {
let mut config = PixelCoverageConfig::default();
config.thresholds.min_coverage = 150.0; assert!(!config.is_valid());
assert!(!config.validate().is_empty());
}
#[test]
fn h0_config_06_validation_ssim() {
let mut config = PixelCoverageConfig::default();
config.verification.ssim_threshold = 1.5; let errors = config.validate();
assert!(errors.iter().any(|e| e.field.contains("ssim")));
}
#[test]
fn h0_config_07_validation_palette() {
let mut config = PixelCoverageConfig::default();
config.output.palette = "invalid".to_string();
let errors = config.validate();
assert!(errors.iter().any(|e| e.field.contains("palette")));
}
#[test]
fn h0_config_08_normalized() {
let config = PixelCoverageConfig::default();
assert!((config.normalized_min_coverage() - 0.85).abs() < f32::EPSILON);
assert!((config.normalized_max_gap() - 0.05).abs() < f32::EPSILON);
}
#[test]
fn h0_config_09_threshold_defaults() {
let threshold = ThresholdConfig::default();
assert!((threshold.falsifiability_threshold - 15.0).abs() < f32::EPSILON);
}
#[test]
fn h0_config_10_performance_defaults() {
let perf = PerformanceConfig::default();
assert!(perf.parallel);
assert_eq!(perf.threads, 0);
assert!(perf.enable_downscaling);
}
#[test]
fn h0_config_11_output_defaults() {
let output = OutputConfig::default();
assert!(output.heatmap);
assert!(output.terminal_gui);
assert_eq!(output.palette, "viridis");
}
#[test]
fn h0_config_12_verification_defaults() {
let verify = VerificationConfig::default();
assert!((verify.ssim_threshold - 0.99).abs() < f32::EPSILON);
assert_eq!(verify.phash_distance, 5);
}
#[test]
fn h0_config_13_downscale_validation() {
let mut config = PixelCoverageConfig::default();
config.performance.downscale_factor = 0; let errors = config.validate();
assert!(errors.iter().any(|e| e.field.contains("downscale")));
}
#[test]
fn h0_config_14_error_display() {
let error = ConfigValidationError {
field: "test.field".to_string(),
message: "test message".to_string(),
};
let display = format!("{}", error);
assert!(display.contains("test.field"));
assert!(display.contains("test message"));
}
}