use super::*;
use crate::repairability_index::parameters::PriorityPartScores;
use crate::ruleset::Ruleset;
fn flat(score: u8, folding: Option<u8>) -> PriorityPartScores {
PriorityPartScores {
battery: score,
display_assembly: score,
back_cover: score,
front_camera: score,
rear_camera: score,
charging_port: score,
mechanical_button: score,
microphone: score,
speaker: score,
folding_mechanism: folding,
}
}
fn uniform(score: u8, folding: Option<u8>) -> RepairabilityIndexInputs {
RepairabilityIndexInputs {
disassembly_depth: flat(score, folding),
fasteners: flat(score, folding),
tools: flat(score, folding),
spare_parts: score,
software_updates: score,
repair_information: score,
}
}
#[test]
fn part_weights_sum_to_one() {
let r = Eu2023_1669Ruleset;
for foldable in [false, true] {
let w = r.part_weights(foldable);
let total = w.battery
+ w.display_assembly
+ w.back_cover
+ w.minor_part * 6.0
+ w.folding_mechanism.unwrap_or(0.0);
assert!(
(total - 1.0).abs() < 1e-9,
"foldable={foldable} part weights sum to {total}, expected 1"
);
}
}
#[test]
fn parameter_weights_sum_to_one() {
let w = Eu2023_1669Ruleset.weights();
let total = w.disassembly_depth
+ w.fasteners
+ w.tools
+ w.spare_parts
+ w.software_updates
+ w.repair_information;
assert!((total - 1.0).abs() < 1e-9, "weights sum to {total}");
}
#[test]
fn all_fives_is_the_maximum_index_and_class_a() {
let r = calculate(&uniform(5, None), &Eu2023_1669Ruleset).unwrap();
assert!((r.index - 5.0).abs() < 1e-9, "index was {}", r.index);
assert_eq!(r.class, RepairabilityClass::A);
}
#[test]
fn all_ones_is_the_minimum_index_and_class_e() {
let r = calculate(&uniform(1, None), &Eu2023_1669Ruleset).unwrap();
assert!((r.index - 1.0).abs() < 1e-9, "index was {}", r.index);
assert_eq!(r.class, RepairabilityClass::E);
}
#[test]
fn foldable_weight_set_also_spans_one_to_five() {
let lo = calculate(&uniform(1, Some(1)), &Eu2023_1669Ruleset).unwrap();
let hi = calculate(&uniform(5, Some(5)), &Eu2023_1669Ruleset).unwrap();
assert!((lo.index - 1.0).abs() < 1e-9);
assert!((hi.index - 5.0).abs() < 1e-9);
assert!(hi.foldable);
}
#[test]
fn class_boundaries_match_annex_ii_table_4() {
let a = calculate(&uniform(4, None), &Eu2023_1669Ruleset).unwrap();
assert!((a.index - 4.0).abs() < 1e-9);
assert_eq!(a.class, RepairabilityClass::A, "R = 4,00 is class A");
let c = calculate(&uniform(3, None), &Eu2023_1669Ruleset).unwrap();
assert_eq!(c.class, RepairabilityClass::C);
let d = calculate(&uniform(2, None), &Eu2023_1669Ruleset).unwrap();
assert_eq!(d.class, RepairabilityClass::D);
let e = calculate(&uniform(1, None), &Eu2023_1669Ruleset).unwrap();
assert_eq!(e.class, RepairabilityClass::E);
let mut b_inputs = uniform(3, None);
b_inputs.spare_parts = 5;
b_inputs.software_updates = 5;
b_inputs.repair_information = 5;
let b = calculate(&b_inputs, &Eu2023_1669Ruleset).unwrap();
assert!((b.index - 3.90).abs() < 1e-9, "index was {}", b.index);
assert_eq!(b.class, RepairabilityClass::B);
}
#[test]
fn just_below_an_edge_drops_a_class() {
let bounds = Eu2023_1669Ruleset.class_boundaries();
let just_under_a = calculate(&uniform(4, None), &Eu2023_1669Ruleset)
.unwrap()
.index
- 0.01;
assert!(just_under_a < bounds.a);
assert!(just_under_a >= bounds.b, "should land in B, not lower");
}
#[test]
fn score_outside_one_to_five_is_rejected() {
let mut i = uniform(3, None);
i.spare_parts = 0;
assert!(matches!(
calculate(&i, &Eu2023_1669Ruleset),
Err(crate::error::CalcError::InvalidInput(_))
));
let mut j = uniform(3, None);
j.disassembly_depth.battery = 6;
assert!(matches!(
calculate(&j, &Eu2023_1669Ruleset),
Err(crate::error::CalcError::InvalidInput(_))
));
}
#[test]
fn inconsistent_foldability_is_rejected() {
let inputs = RepairabilityIndexInputs {
disassembly_depth: flat(3, Some(3)),
fasteners: flat(3, None),
tools: flat(3, Some(3)),
spare_parts: 3,
software_updates: 3,
repair_information: 3,
};
assert!(matches!(
calculate(&inputs, &Eu2023_1669Ruleset),
Err(crate::error::CalcError::CrossFieldViolation(_))
));
}
#[test]
fn disassembly_depth_dominates_the_other_parameters() {
let mut drop_sdd = uniform(5, None);
drop_sdd.disassembly_depth = flat(1, None);
let mut drop_tools = uniform(5, None);
drop_tools.tools = flat(1, None);
let a = calculate(&drop_sdd, &Eu2023_1669Ruleset).unwrap().index;
let b = calculate(&drop_tools, &Eu2023_1669Ruleset).unwrap().index;
assert!(a < b, "SDD ({a}) should cost more than tools ({b})");
}
#[test]
fn ruleset_is_effective_from_the_regulation_date() {
let r = Eu2023_1669Ruleset;
assert_eq!(
*r.effectivity(),
crate::ruleset::Effectivity::open(chrono::NaiveDate::from_ymd_opt(2025, 6, 20).unwrap())
);
assert!(!r.regulatory_basis().regulation.is_empty());
}
#[test]
fn index_and_heuristic_use_different_scales() {
let idx = calculate(&uniform(5, None), &Eu2023_1669Ruleset).unwrap();
assert!(idx.index <= 5.0);
assert_ne!(
Eu2023_1669Ruleset.id().0,
crate::repairability::SimplifiedRepairabilityHeuristic
.id()
.0
);
}
#[test]
fn index_is_rounded_to_two_decimals_before_classification() {
let b = Eu2023_1669Ruleset.class_boundaries();
assert!(
(b.a * 100.0).fract().abs() < 1e-9,
"class boundaries are stated to two decimals: {}",
b.a
);
let just_under = b.a - 0.004;
assert_eq!((just_under * 100.0).round() / 100.0, b.a);
}