#[cfg(test)]
mod property_tests {
use crate::{MunsellConverter, MunsellColor};
use proptest::prelude::*;
use std::sync::Arc;
proptest! {
#[test]
fn prop_all_rgb_values_convert_successfully(
r in 0u8..=255,
g in 0u8..=255,
b in 0u8..=255
) {
let converter = MunsellConverter::new().unwrap();
let result = converter.srgb_to_munsell([r, g, b]);
prop_assert!(result.is_ok(), "Failed to convert RGB[{}, {}, {}]: {:?}", r, g, b, result);
let munsell = result.unwrap();
prop_assert!(munsell.value >= 0.0 && munsell.value <= 10.0,
"Value out of range: {}", munsell.value);
if let Some(chroma) = munsell.chroma {
prop_assert!(chroma >= 0.0, "Negative chroma: {}", chroma);
prop_assert!(chroma <= 50.0, "Unreasonably high chroma: {}", chroma); }
}
}
proptest! {
#[test]
fn prop_rgb_conversion_deterministic(
r in 0u8..=255,
g in 0u8..=255,
b in 0u8..=255
) {
let converter = MunsellConverter::new().unwrap();
let rgb = [r, g, b];
let result1 = converter.srgb_to_munsell(rgb).unwrap();
let result2 = converter.srgb_to_munsell(rgb).unwrap();
let result3 = converter.srgb_to_munsell(rgb).unwrap();
prop_assert_eq!(&result1.notation, &result2.notation, "Non-deterministic conversion for RGB{:?}", rgb);
prop_assert_eq!(&result1.notation, &result3.notation, "Non-deterministic conversion for RGB{:?}", rgb);
prop_assert_eq!(result1.value, result2.value, "Non-deterministic value for RGB{:?}", rgb);
prop_assert_eq!(result1.chroma, result2.chroma, "Non-deterministic chroma for RGB{:?}", rgb);
}
}
proptest! {
#[test]
fn prop_batch_conversion_equals_individual(
colors in prop::collection::vec(
(0u8..=255, 0u8..=255, 0u8..=255), 1..=10
)
) {
let converter = MunsellConverter::new().unwrap();
let rgb_colors: Vec<[u8; 3]> = colors.into_iter()
.map(|(r, g, b)| [r, g, b])
.collect();
let individual_results: Result<Vec<_>, _> = rgb_colors.iter()
.map(|&rgb| converter.srgb_to_munsell(rgb))
.collect();
let individual_results = individual_results.unwrap();
let batch_results = converter.convert_batch(&rgb_colors).unwrap();
prop_assert_eq!(individual_results.len(), batch_results.len());
for (i, (individual, batch)) in individual_results.iter().zip(batch_results.iter()).enumerate() {
prop_assert_eq!(&individual.notation, &batch.notation,
"Batch/individual mismatch at index {}: individual='{}', batch='{}'",
i, individual.notation, batch.notation);
}
}
}
proptest! {
#[test]
fn prop_grayscale_colors_neutral(
value in 0u8..=255
) {
let converter = MunsellConverter::new().unwrap();
let gray_rgb = [value, value, value];
let result = converter.srgb_to_munsell(gray_rgb).unwrap();
if result.is_neutral() {
prop_assert!(result.hue.is_none(), "Neutral color should have no hue");
prop_assert!(result.chroma.is_none(), "Neutral color should have no chroma");
prop_assert!(result.notation.starts_with('N'), "Neutral notation should start with 'N'");
} else {
if let Some(chroma) = result.chroma {
prop_assert!(chroma < 2.0,
"Grayscale RGB{:?} should have low chroma, got {}", gray_rgb, chroma);
}
}
}
}
#[test]
fn prop_black_always_neutral_zero() {
let converter = MunsellConverter::new().unwrap();
let black = converter.srgb_to_munsell([0, 0, 0]).unwrap();
assert_eq!(black.notation, "N 0.0");
assert!(black.is_neutral());
assert_eq!(black.value, 0.0);
assert!(black.hue.is_none());
assert!(black.chroma.is_none());
}
proptest! {
#[test]
fn prop_luminance_correlates_with_value(
base_value in 10u8..=200, increment in 10u8..=50
) {
let converter = MunsellConverter::new().unwrap();
let darker_rgb = [base_value, base_value, base_value];
let lighter_rgb = [
(base_value.saturating_add(increment)).min(255),
(base_value.saturating_add(increment)).min(255),
(base_value.saturating_add(increment)).min(255)
];
let darker_result = converter.srgb_to_munsell(darker_rgb).unwrap();
let lighter_result = converter.srgb_to_munsell(lighter_rgb).unwrap();
prop_assert!(lighter_result.value >= darker_result.value,
"Lighter RGB{:?} (value={}) should have higher value than darker RGB{:?} (value={})",
lighter_rgb, lighter_result.value, darker_rgb, darker_result.value);
}
}
proptest! {
#[test]
fn prop_thread_safety(
colors in prop::collection::vec(
(0u8..=255, 0u8..=255, 0u8..=255), 5..=15
)
) {
let converter = Arc::new(MunsellConverter::new().unwrap());
let rgb_colors: Vec<[u8; 3]> = colors.into_iter()
.map(|(r, g, b)| [r, g, b])
.collect();
let main_results: Vec<_> = rgb_colors.iter()
.map(|&rgb| converter.srgb_to_munsell(rgb).unwrap().notation)
.collect();
let converter_clone = Arc::clone(&converter);
let colors_clone = rgb_colors.clone();
let handle = std::thread::spawn(move || {
colors_clone.iter()
.map(|&rgb| converter_clone.srgb_to_munsell(rgb).unwrap().notation)
.collect::<Vec<_>>()
});
let thread_results = handle.join().unwrap();
prop_assert_eq!(main_results, thread_results,
"Thread safety violation: results differ between threads");
}
}
proptest! {
#[test]
fn prop_munsell_notation_parsing_roundtrip(
r in 0u8..=255,
g in 0u8..=255,
b in 0u8..=255
) {
let converter = MunsellConverter::new().unwrap();
let rgb = [r, g, b];
let munsell = converter.srgb_to_munsell(rgb).unwrap();
let notation = munsell.notation.clone();
let parsed = MunsellColor::from_notation(¬ation);
prop_assert!(parsed.is_ok(), "Failed to parse generated notation '{}' for RGB{:?}", notation, rgb);
let parsed_munsell = parsed.unwrap();
prop_assert_eq!(&munsell.notation, &parsed_munsell.notation);
prop_assert_eq!(munsell.is_neutral(), parsed_munsell.is_neutral());
prop_assert!((munsell.value - parsed_munsell.value).abs() < 0.01,
"Value mismatch: original={}, parsed={}", munsell.value, parsed_munsell.value);
}
}
proptest! {
#[test]
fn prop_extreme_rgb_values_safe(
extreme_pattern in 0u8..=7 ) {
let converter = MunsellConverter::new().unwrap();
let extreme_values = [0, 255];
let r = extreme_values[(extreme_pattern & 1) as usize];
let g = extreme_values[((extreme_pattern >> 1) & 1) as usize];
let b = extreme_values[((extreme_pattern >> 2) & 1) as usize];
let rgb = [r, g, b];
let result = converter.srgb_to_munsell(rgb);
prop_assert!(result.is_ok(), "Failed to convert extreme RGB{:?}: {:?}", rgb, result);
let munsell = result.unwrap();
prop_assert!(munsell.value >= 0.0 && munsell.value <= 10.0);
if let Some(chroma) = munsell.chroma {
prop_assert!(chroma >= 0.0 && chroma.is_finite());
}
prop_assert!(!munsell.notation.is_empty());
prop_assert!(munsell.notation.len() < 20); }
}
}