#[cfg(test)] mod integration_tests { use rota_lib::{ config::{ToxicPair, UserConfig}, resident::Resident, schedule::{MonthlySchedule, ShiftType}, scheduler::Scheduler, slot::{Day, ShiftPosition, Slot}, workload::{WorkloadBounds, WorkloadTracker}, }; use rstest::{fixture, rstest}; #[fixture] fn minimal_config() -> UserConfig { UserConfig::default().with_residents(vec![ Resident::new(1, "R1"), Resident::new(2, "R2"), Resident::new(3, "R3"), Resident::new(4, "R4"), ]) } #[fixture] fn maximal_config() -> UserConfig { UserConfig::default() .with_holidays(vec![2, 3, 10, 11, 12, 25]) .with_residents(vec![ Resident::new(1, "R1").with_max_shifts(3), Resident::new(2, "R2").with_max_shifts(4), Resident::new(3, "R3").with_reduced_load(), Resident::new(4, "R4").with_allowed_types(vec![ShiftType::Closed]), Resident::new(5, "R5") .with_allowed_types(vec![ShiftType::OpenFirst, ShiftType::OpenSecond]), Resident::new(6, "R6").with_negative_shifts(vec![Day(5), Day(15), Day(25)]), Resident::new(7, "R7"), Resident::new(8, "R8"), Resident::new(9, "R9"), Resident::new(10, "R10"), ]) .with_toxic_pairs(vec![ ToxicPair::new(1, 2), ToxicPair::new(3, 4), ToxicPair::new(7, 8), ]) } #[fixture] fn manual_shifts_heavy_config() -> UserConfig { UserConfig::default().with_residents(vec![ Resident::new(1, "R1").with_manual_shifts(vec![ Slot::new(Day(1), ShiftPosition::First), Slot::new(Day(3), ShiftPosition::First), Slot::new(Day(5), ShiftPosition::Second), ]), Resident::new(2, "R2").with_manual_shifts(vec![ Slot::new(Day(2), ShiftPosition::First), Slot::new(Day(4), ShiftPosition::First), ]), Resident::new(3, "R3"), Resident::new(4, "R4"), Resident::new(5, "R5"), Resident::new(6, "R6"), ]) } #[fixture] fn complex_config() -> UserConfig { UserConfig::default() .with_holidays(vec![5, 12, 19, 26]) .with_residents(vec![ Resident::new(1, "R1") .with_max_shifts(3) .with_negative_shifts(vec![Day(1), Day(2), Day(3)]), Resident::new(2, "R2") .with_max_shifts(3) .with_negative_shifts(vec![Day(4), Day(5), Day(6)]), Resident::new(3, "R3") .with_max_shifts(3) .with_negative_shifts(vec![Day(7), Day(8), Day(9)]), Resident::new(4, "R4").with_allowed_types(vec![ShiftType::Closed]), Resident::new(5, "R5") .with_allowed_types(vec![ShiftType::OpenFirst, ShiftType::OpenSecond]), Resident::new(6, "R6"), Resident::new(7, "R7"), Resident::new(8, "R8"), ]) .with_toxic_pairs(vec![ ToxicPair::new(1, 2), ToxicPair::new(2, 3), ToxicPair::new(5, 6), ToxicPair::new(6, 7), ]) } #[rstest] fn test_minimal_config(minimal_config: UserConfig) -> anyhow::Result<()> { let mut schedule = MonthlySchedule::new(); let mut tracker = WorkloadTracker::default(); let scheduler = Scheduler::new_with_config(minimal_config.clone()); assert!(scheduler.run(&mut schedule, &mut tracker)?); validate_all_constraints(&schedule, &tracker, &minimal_config); Ok(()) } #[rstest] fn test_maximal_config(maximal_config: UserConfig) -> anyhow::Result<()> { let mut schedule = MonthlySchedule::new(); let mut tracker = WorkloadTracker::default(); let scheduler = Scheduler::new_with_config(maximal_config.clone()); assert!(scheduler.run(&mut schedule, &mut tracker)?); validate_all_constraints(&schedule, &tracker, &maximal_config); Ok(()) } #[rstest] fn test_manual_shifts_heavy_config( manual_shifts_heavy_config: UserConfig, ) -> anyhow::Result<()> { let mut schedule = MonthlySchedule::new(); let mut tracker = WorkloadTracker::default(); let scheduler = Scheduler::new_with_config(manual_shifts_heavy_config.clone()); assert!(scheduler.run(&mut schedule, &mut tracker)?); validate_all_constraints(&schedule, &tracker, &manual_shifts_heavy_config); Ok(()) } #[rstest] fn test_complex_config(complex_config: UserConfig) -> anyhow::Result<()> { let mut schedule = MonthlySchedule::new(); let mut tracker = WorkloadTracker::default(); let scheduler = Scheduler::new_with_config(complex_config.clone()); assert!(scheduler.run(&mut schedule, &mut tracker)?); validate_all_constraints(&schedule, &tracker, &complex_config); Ok(()) } fn validate_all_constraints( schedule: &MonthlySchedule, tracker: &WorkloadTracker, config: &UserConfig, ) { assert_eq!(schedule.0.len() as u8, config.total_slots); for d in 2..=config.total_days { let current: Vec<_> = [ShiftPosition::First, ShiftPosition::Second] .iter() .filter_map(|&p| schedule.get_resident_id(&Slot::new(Day(d), p))) .collect(); let previous: Vec<_> = [ShiftPosition::First, ShiftPosition::Second] .iter() .filter_map(|&p| schedule.get_resident_id(&Slot::new(Day(d - 1), p))) .collect(); for res in current { assert!(!previous.contains(&res)); } } for d in 1..=config.total_days { let day = Day(d); if day.is_open_shift() { let r1 = schedule.get_resident_id(&Slot::new(day, ShiftPosition::First)); let r2 = schedule.get_resident_id(&Slot::new(day, ShiftPosition::Second)); assert_ne!(r1, r2); if let (Some(id1), Some(id2)) = (r1, r2) { let pair = ToxicPair::from((*id1, *id2)); assert!(config.toxic_pairs.iter().all(|t| !t.matches(&pair))); } } } let bounds = WorkloadBounds::new_with_config(config); for (slot, res_id) in &schedule.0 { let res = config .residents .iter() .find(|r| &r.id == res_id) .expect("Resident not found"); assert!(res.allowed_types.contains(&slot.shift_type())); assert!(!res.negative_shifts.contains(&slot.day)); } for resident in &config.residents { let workload = tracker.current_workload(&resident.id); let max = *bounds.max_workloads.get(&resident.id).unwrap(); assert!(workload <= max); } } }