#![allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TestCardType {
ColorBars,
Grey,
Pluge,
Checkfield,
}
impl TestCardType {
#[must_use]
pub fn label(&self) -> &'static str {
match self {
Self::ColorBars => "Colour Bars",
Self::Grey => "Grey Field",
Self::Pluge => "PLUGE",
Self::Checkfield => "Checkfield",
}
}
}
#[derive(Clone, Debug)]
pub struct TestCardSpec {
pub width: u32,
pub height: u32,
pub frame_rate_num: u32,
pub frame_rate_den: u32,
pub duration_seconds: f64,
pub card_type: TestCardType,
}
impl TestCardSpec {
#[must_use]
pub fn new(
width: u32,
height: u32,
frame_rate_num: u32,
frame_rate_den: u32,
duration_seconds: f64,
card_type: TestCardType,
) -> Self {
Self {
width,
height,
frame_rate_num,
frame_rate_den,
duration_seconds,
card_type,
}
}
#[must_use]
pub fn is_hd(&self) -> bool {
self.width >= 1280
}
#[must_use]
pub fn is_uhd(&self) -> bool {
self.width >= 3840
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn frame_rate(&self) -> f64 {
f64::from(self.frame_rate_num) / f64::from(self.frame_rate_den)
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn total_frames(&self) -> u64 {
(self.duration_seconds * self.frame_rate()) as u64
}
}
#[derive(Clone, Debug)]
pub struct ValidationIssue {
pub description: String,
pub is_fatal: bool,
}
#[derive(Clone, Debug, Default)]
pub struct TestCardValidator {
issues: Vec<ValidationIssue>,
}
impl TestCardValidator {
#[must_use]
pub fn new() -> Self {
Self { issues: Vec::new() }
}
pub fn validate_levels(&mut self, white_ire: f32, black_ire: f32) {
if white_ire > 100.0 {
self.issues.push(ValidationIssue {
description: format!("White level {white_ire:.1} IRE exceeds 100 IRE limit"),
is_fatal: true,
});
}
if black_ire < 0.0 {
self.issues.push(ValidationIssue {
description: format!(
"Black level {black_ire:.1} IRE is below 0 IRE (crushed blacks)"
),
is_fatal: false,
});
}
if (white_ire - black_ire) < 70.0 {
self.issues.push(ValidationIssue {
description: format!(
"Contrast range {:.1} IRE is less than the recommended 70 IRE minimum",
white_ire - black_ire
),
is_fatal: false,
});
}
}
pub fn validate_saturation(&mut self, max_saturation: f32) {
if max_saturation > 1.0 {
self.issues.push(ValidationIssue {
description: format!("Saturation {max_saturation:.3} exceeds legal limit of 1.0"),
is_fatal: true,
});
}
}
#[must_use]
pub fn issues(&self) -> &[ValidationIssue] {
&self.issues
}
#[must_use]
pub fn has_fatal_issues(&self) -> bool {
self.issues.iter().any(|i| i.is_fatal)
}
pub fn reset(&mut self) {
self.issues.clear();
}
}
#[derive(Clone, Debug)]
pub struct TestCardReport {
pub spec: TestCardSpec,
pub issues: Vec<ValidationIssue>,
}
impl TestCardReport {
#[must_use]
pub fn new(spec: TestCardSpec, issues: Vec<ValidationIssue>) -> Self {
Self { spec, issues }
}
#[must_use]
pub fn passes(&self) -> bool {
!self.issues.iter().any(|i| i.is_fatal)
}
#[must_use]
pub fn warning_count(&self) -> usize {
self.issues.iter().filter(|i| !i.is_fatal).count()
}
#[must_use]
pub fn error_count(&self) -> usize {
self.issues.iter().filter(|i| i.is_fatal).count()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_card_type_label_color_bars() {
assert_eq!(TestCardType::ColorBars.label(), "Colour Bars");
}
#[test]
fn test_card_type_label_grey() {
assert_eq!(TestCardType::Grey.label(), "Grey Field");
}
#[test]
fn test_card_type_label_pluge() {
assert_eq!(TestCardType::Pluge.label(), "PLUGE");
}
#[test]
fn test_card_type_label_checkfield() {
assert_eq!(TestCardType::Checkfield.label(), "Checkfield");
}
#[test]
fn test_spec_is_hd_true() {
let spec = TestCardSpec::new(1920, 1080, 25, 1, 10.0, TestCardType::ColorBars);
assert!(spec.is_hd());
}
#[test]
fn test_spec_is_hd_false_for_sd() {
let spec = TestCardSpec::new(720, 576, 25, 1, 10.0, TestCardType::ColorBars);
assert!(!spec.is_hd());
}
#[test]
fn test_spec_is_uhd() {
let spec = TestCardSpec::new(3840, 2160, 50, 1, 5.0, TestCardType::Grey);
assert!(spec.is_uhd());
assert!(spec.is_hd());
}
#[test]
fn test_spec_frame_rate() {
let spec = TestCardSpec::new(1920, 1080, 30000, 1001, 1.0, TestCardType::Grey);
let fps = spec.frame_rate();
assert!((fps - 29.97).abs() < 0.01);
}
#[test]
fn test_spec_total_frames() {
let spec = TestCardSpec::new(1920, 1080, 25, 1, 4.0, TestCardType::Pluge);
assert_eq!(spec.total_frames(), 100);
}
#[test]
fn test_validator_no_issues_for_good_levels() {
let mut v = TestCardValidator::new();
v.validate_levels(100.0, 7.5);
assert!(!v.has_fatal_issues());
assert!(v.issues().is_empty());
}
#[test]
fn test_validator_white_too_high() {
let mut v = TestCardValidator::new();
v.validate_levels(105.0, 7.5);
assert!(v.has_fatal_issues());
}
#[test]
fn test_validator_black_below_zero() {
let mut v = TestCardValidator::new();
v.validate_levels(100.0, -5.0);
let non_fatal: Vec<_> = v.issues().iter().filter(|i| !i.is_fatal).collect();
assert!(!non_fatal.is_empty());
}
#[test]
fn test_validator_low_contrast_warning() {
let mut v = TestCardValidator::new();
v.validate_levels(60.0, 10.0); let warnings: Vec<_> = v.issues().iter().filter(|i| !i.is_fatal).collect();
assert!(!warnings.is_empty());
}
#[test]
fn test_validator_saturation_legal() {
let mut v = TestCardValidator::new();
v.validate_saturation(0.75);
assert!(!v.has_fatal_issues());
}
#[test]
fn test_validator_saturation_illegal() {
let mut v = TestCardValidator::new();
v.validate_saturation(1.05);
assert!(v.has_fatal_issues());
}
#[test]
fn test_validator_reset() {
let mut v = TestCardValidator::new();
v.validate_levels(110.0, -1.0);
assert!(!v.issues().is_empty());
v.reset();
assert!(v.issues().is_empty());
}
#[test]
fn test_report_passes_no_fatal() {
let spec = TestCardSpec::new(1920, 1080, 25, 1, 10.0, TestCardType::ColorBars);
let report = TestCardReport::new(spec, vec![]);
assert!(report.passes());
assert_eq!(report.error_count(), 0);
}
#[test]
fn test_report_fails_with_fatal() {
let spec = TestCardSpec::new(1920, 1080, 25, 1, 10.0, TestCardType::Pluge);
let issues = vec![ValidationIssue {
description: "Over limit".to_string(),
is_fatal: true,
}];
let report = TestCardReport::new(spec, issues);
assert!(!report.passes());
assert_eq!(report.error_count(), 1);
}
#[test]
fn test_report_warning_count() {
let spec = TestCardSpec::new(1920, 1080, 25, 1, 10.0, TestCardType::Grey);
let issues = vec![
ValidationIssue {
description: "warn1".to_string(),
is_fatal: false,
},
ValidationIssue {
description: "warn2".to_string(),
is_fatal: false,
},
ValidationIssue {
description: "err1".to_string(),
is_fatal: true,
},
];
let report = TestCardReport::new(spec, issues);
assert_eq!(report.warning_count(), 2);
assert_eq!(report.error_count(), 1);
assert!(!report.passes());
}
}