From 76d308351a2ea4a5a52b2f1e06dfc9398524c679 Mon Sep 17 00:00:00 2001 From: stefiosif Date: Sun, 22 Feb 2026 13:01:28 +0200 Subject: [PATCH] Reorganize integration tests, simplify fn signatures --- src-tauri/src/config.rs | 69 ++++++++++-- src-tauri/src/export.rs | 72 ------------- src-tauri/src/fixtures.rs | 19 ++-- src-tauri/src/schedule.rs | 78 +++----------- src-tauri/src/scheduler.rs | 62 +---------- src-tauri/src/workload.rs | 189 ++++++++++++++++++--------------- src-tauri/tests/common/mod.rs | 58 ++++++++++ src-tauri/tests/integration.rs | 67 +++--------- 8 files changed, 270 insertions(+), 344 deletions(-) create mode 100644 src-tauri/tests/common/mod.rs diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 36d3b7a..aecac62 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::{ resident::{Resident, ResidentDTO, ResidentId}, schedule::ShiftType, - slot::Day, + slot::{Day, Slot}, }; const MONTH: u8 = 4; @@ -58,6 +58,32 @@ pub struct UserConfig { } impl UserConfig { + pub fn new(month: u8, year: i32) -> Self { + let month = Month::try_from(month).unwrap(); + + let total_days = month.num_days(year).unwrap(); + + let total_slots = (1..=total_days) + .map(|d| if Day(d).is_open_shift() { 2 } else { 1 }) + .sum(); + + let total_holiday_slots = (1..=total_days) + .filter(|&d| Day(d).is_weekend(month.number_from_month(), year)) + .map(|d| if Day(d).is_open_shift() { 2 } else { 1 }) + .sum(); + + Self { + month, + year, + holidays: vec![], + residents: vec![], + toxic_pairs: vec![], + total_days, + total_slots, + total_holiday_slots, + } + } + pub fn with_holidays(mut self, holidays: Vec) -> Self { self.holidays = holidays; self.total_holiday_slots = self.total_holiday_slots(); @@ -80,15 +106,18 @@ impl UserConfig { fn total_holiday_slots(&self) -> u8 { (1..=self.total_days) - .filter(|&d| self.is_holiday_or_weekend_slot(d)) + .filter(|&d| self.is_holiday_or_weekend(Day(d))) .map(|d| if Day(d).is_open_shift() { 2 } else { 1 }) .sum() } - pub fn is_holiday_or_weekend_slot(&self, day: u8) -> bool { - let day = Day(day); - day.is_weekend(self.month.number_from_month(), self.year) - || self.holidays.contains(&(day.0)) + pub fn is_holiday_or_weekend(&self, day: Day) -> bool { + let month = self.month.number_from_month(); + day.is_weekend(month, self.year) || self.holidays.contains(&(day.0)) + } + + pub fn is_holiday_or_weekend_slot(&self, slot: Slot) -> bool { + self.is_holiday_or_weekend(slot.day) } pub fn get_initial_supply(&self) -> HashMap { @@ -174,7 +203,12 @@ impl TryFrom for UserConfig { #[cfg(test)] mod tests { - use crate::{config::UserConfig, fixtures::complex_config, schedule::ShiftType}; + use crate::{ + config::UserConfig, + fixtures::complex_config, + schedule::ShiftType, + slot::{Day, ShiftPosition, Slot}, + }; use rstest::rstest; #[rstest] @@ -185,4 +219,25 @@ mod tests { assert_eq!(15, *supply.get(&ShiftType::OpenSecond).unwrap()); assert_eq!(15, *supply.get(&ShiftType::Closed).unwrap()); } + + #[rstest] + fn test_is_holiday_or_weekend(complex_config: UserConfig) { + assert!(!complex_config.is_holiday_or_weekend(Day(1))); + assert!(complex_config.is_holiday_or_weekend(Day(4))); + assert!(complex_config.is_holiday_or_weekend(Day(5))); + assert!(complex_config.is_holiday_or_weekend(Day(10))); + } + + #[rstest] + fn test_is_holiday_or_weekend_slot(complex_config: UserConfig) { + let weekday = Slot::new(Day(1), ShiftPosition::First); + let sat = Slot::new(Day(4), ShiftPosition::First); + let sun = Slot::new(Day(5), ShiftPosition::First); + let manual_holiday = Slot::new(Day(10), ShiftPosition::First); + + assert!(!complex_config.is_holiday_or_weekend_slot(weekday)); + assert!(complex_config.is_holiday_or_weekend_slot(sat)); + assert!(complex_config.is_holiday_or_weekend_slot(sun)); + assert!(complex_config.is_holiday_or_weekend_slot(manual_holiday)); + } } diff --git a/src-tauri/src/export.rs b/src-tauri/src/export.rs index 38c60e7..687fe11 100644 --- a/src-tauri/src/export.rs +++ b/src-tauri/src/export.rs @@ -172,75 +172,3 @@ impl MonthlySchedule { Ok(()) } } - -#[cfg(test)] -mod tests { - use rstest::{fixture, rstest}; - - use crate::{ - config::UserConfig, - export::{Export, FileType}, - resident::Resident, - schedule::MonthlySchedule, - scheduler::Scheduler, - workload::{WorkloadBounds, WorkloadTracker}, - }; - - #[fixture] - fn schedule() -> MonthlySchedule { - MonthlySchedule::new() - } - - #[fixture] - fn config() -> UserConfig { - UserConfig::default().with_residents(vec![ - Resident::new(1, "R1"), - Resident::new(2, "R2"), - Resident::new(3, "R3"), - Resident::new(4, "R4"), - Resident::new(5, "R5"), - Resident::new(6, "R6"), - ]) - } - - #[fixture] - fn bounds(config: UserConfig) -> WorkloadBounds { - WorkloadBounds::new_with_config(&config) - } - - #[fixture] - fn scheduler(config: UserConfig, bounds: WorkloadBounds) -> Scheduler { - Scheduler::new(config, bounds) - } - - #[fixture] - fn tracker() -> WorkloadTracker { - WorkloadTracker::default() - } - - #[rstest] - pub fn test_export_as_txt( - mut schedule: MonthlySchedule, - mut tracker: WorkloadTracker, - scheduler: Scheduler, - ) -> anyhow::Result<()> { - assert!(scheduler.run(&mut schedule, &mut tracker)?); - - schedule.export(FileType::Txt, &scheduler.config, &tracker)?; - - Ok(()) - } - - #[rstest] - pub fn test_generate_docx( - mut schedule: MonthlySchedule, - mut tracker: WorkloadTracker, - scheduler: Scheduler, - ) -> anyhow::Result<()> { - assert!(scheduler.run(&mut schedule, &mut tracker)?); - - schedule.generate_docx(&scheduler.config); - - Ok(()) - } -} diff --git a/src-tauri/src/fixtures.rs b/src-tauri/src/fixtures.rs index e27d79d..768118b 100644 --- a/src-tauri/src/fixtures.rs +++ b/src-tauri/src/fixtures.rs @@ -5,11 +5,12 @@ use crate::{ slot::{Day, ShiftPosition, Slot}, }; +use chrono::Month; use rstest::fixture; #[fixture] pub fn minimal_config() -> UserConfig { - UserConfig::default().with_residents(vec![ + UserConfig::new(Month::April.number_from_month() as u8, 2026).with_residents(vec![ Resident::new(1, "R1"), Resident::new(2, "R2"), Resident::new(3, "R3"), @@ -20,7 +21,7 @@ pub fn minimal_config() -> UserConfig { #[fixture] pub fn maximal_config() -> UserConfig { - UserConfig::default() + UserConfig::new(Month::April.number_from_month() as u8, 2026) .with_holidays(vec![2, 3, 10, 11, 12, 25]) .with_residents(vec![ Resident::new(1, "R1").with_max_shifts(3), @@ -30,21 +31,23 @@ pub fn maximal_config() -> UserConfig { 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(7, "R7") + .with_allowed_types(vec![ShiftType::OpenFirst, ShiftType::OpenSecond]), Resident::new(8, "R8"), Resident::new(9, "R9"), - Resident::new(10, "R10"), + Resident::new(10, "R10").with_reduced_load(), ]) .with_toxic_pairs(vec![ ToxicPair::new(1, 2), ToxicPair::new(3, 4), + ToxicPair::new(3, 5), ToxicPair::new(7, 8), ]) } #[fixture] pub fn manual_shifts_heavy_config() -> UserConfig { - UserConfig::default().with_residents(vec![ + UserConfig::new(Month::April.number_from_month() as u8, 2026).with_residents(vec![ Resident::new(1, "R1").with_manual_shifts(vec![ Slot::new(Day(1), ShiftPosition::First), Slot::new(Day(3), ShiftPosition::First), @@ -63,8 +66,8 @@ pub fn manual_shifts_heavy_config() -> UserConfig { #[fixture] pub fn complex_config() -> UserConfig { - UserConfig::default() - .with_holidays(vec![5, 12, 19, 26]) + UserConfig::new(Month::April.number_from_month() as u8, 2026) + .with_holidays(vec![5, 10, 12, 19]) .with_residents(vec![ Resident::new(1, "R1") .with_max_shifts(3) @@ -93,7 +96,7 @@ pub fn complex_config() -> UserConfig { #[fixture] pub fn hard_config() -> UserConfig { - UserConfig::default() + UserConfig::new(Month::April.number_from_month() as u8, 2026) .with_holidays(vec![25]) .with_residents(vec![ Resident::new(1, "R1") diff --git a/src-tauri/src/schedule.rs b/src-tauri/src/schedule.rs index fe37b87..160d0b5 100644 --- a/src-tauri/src/schedule.rs +++ b/src-tauri/src/schedule.rs @@ -2,7 +2,7 @@ use serde::{ser::SerializeMap, Deserialize, Serialize}; use std::collections::HashMap; use crate::{ - config::{ToxicPair, UserConfig}, + config::UserConfig, resident::ResidentId, slot::{weekday_to_greek, Day, ShiftPosition, Slot}, workload::WorkloadTracker, @@ -75,24 +75,6 @@ impl MonthlySchedule { || self.get_resident_id(&second) == Some(&res_id) } - pub fn has_toxic_pair(&self, slot: &Slot, config: &UserConfig) -> bool { - if !slot.is_open_shift() { - return false; - } - - let first_id = self.get_resident_id(&slot.previous()); - let second_id = self.get_resident_id(slot); - - if let (Some(r1), Some(r2)) = (first_id, second_id) { - return config - .toxic_pairs - .iter() - .any(|pair| pair.matches(&ToxicPair::from((*r1, *r2)))); - } - - false - } - pub fn pretty_print(&self, config: &UserConfig) -> String { let mut sorted: Vec<_> = self.0.iter().collect(); sorted.sort_by_key(|(slot, _)| (slot.day, slot.position)); @@ -135,9 +117,9 @@ impl MonthlySchedule { for r in residents { let total = tracker.current_workload(&r.id); - let o1 = tracker.get_type_count(&r.id, ShiftType::OpenFirst); - let o2 = tracker.get_type_count(&r.id, ShiftType::OpenSecond); - let cl = tracker.get_type_count(&r.id, ShiftType::Closed); + let o1 = tracker.current_shift_type_workload(&r.id, ShiftType::OpenFirst); + let o2 = tracker.current_shift_type_workload(&r.id, ShiftType::OpenSecond); + let cl = tracker.current_shift_type_workload(&r.id, ShiftType::Closed); let holiday = tracker.current_holiday_workload(&r.id); output.push_str(&format!( @@ -178,39 +160,18 @@ pub enum ShiftType { #[cfg(test)] mod tests { - use rstest::{fixture, rstest}; + use rstest::rstest; use crate::{ - config::{ToxicPair, UserConfig}, resident::{Resident, ResidentId}, schedule::{Day, MonthlySchedule, Slot}, slot::ShiftPosition, }; - #[fixture] - fn schedule() -> MonthlySchedule { - MonthlySchedule::new() - } - - #[fixture] - fn resident() -> Resident { - Resident::new(1, "R1") - } - - #[fixture] - fn toxic_config() -> UserConfig { - UserConfig::default() - .with_residents(vec![Resident::new(1, "R1"), Resident::new(2, "R2")]) - .with_toxic_pairs(vec![ToxicPair::new(1, 2)]) - } - - #[fixture] - fn config() -> UserConfig { - UserConfig::default().with_residents(vec![Resident::new(1, "R1"), Resident::new(2, "R2")]) - } - #[rstest] - fn test_insert_resident(mut schedule: MonthlySchedule, resident: Resident) { + fn test_insert_resident() { + let mut schedule = MonthlySchedule::new(); + let resident = Resident::new(1, "R1"); let slot_1 = Slot::new(Day(1), ShiftPosition::First); let slot_2 = Slot::new(Day(1), ShiftPosition::Second); @@ -221,7 +182,9 @@ mod tests { } #[rstest] - fn test_remove_resident(mut schedule: MonthlySchedule, resident: Resident) { + fn test_remove_resident() { + let mut schedule = MonthlySchedule::new(); + let resident = Resident::new(1, "R1"); let slot_1 = Slot::new(Day(1), ShiftPosition::First); schedule.insert(slot_1, resident.id); @@ -231,7 +194,10 @@ mod tests { } #[rstest] - fn test_same_resident_in_consecutive_days(mut schedule: MonthlySchedule, resident: Resident) { + fn test_same_resident_in_consecutive_days() { + let mut schedule = MonthlySchedule::new(); + let resident = Resident::new(1, "R1"); + let slot_1 = Slot::new(Day(1), ShiftPosition::First); let slot_2 = Slot::new(Day(1), ShiftPosition::Second); let slot_3 = Slot::new(Day(2), ShiftPosition::First); @@ -244,18 +210,4 @@ mod tests { assert!(!schedule.has_resident_in_consecutive_days(&slot_2)); assert!(schedule.has_resident_in_consecutive_days(&slot_3)); } - - #[rstest] - fn test_has_toxic_pair(mut schedule: MonthlySchedule, toxic_config: UserConfig) { - let slot_1 = Slot::new(Day(1), ShiftPosition::First); - let slot_2 = Slot::new(Day(1), ShiftPosition::Second); - - let r1 = &toxic_config.residents[0]; - let r2 = &toxic_config.residents[1]; - - schedule.insert(slot_1, r1.id); - schedule.insert(slot_2, r2.id); - - assert!(schedule.has_toxic_pair(&slot_2, &toxic_config)) - } } diff --git a/src-tauri/src/scheduler.rs b/src-tauri/src/scheduler.rs index 2be90c0..b107942 100644 --- a/src-tauri/src/scheduler.rs +++ b/src-tauri/src/scheduler.rs @@ -131,7 +131,7 @@ impl Scheduler { let mut valid_resident_ids = self.valid_residents(slot, schedule, tracker); valid_resident_ids.shuffle(&mut rng); valid_resident_ids.sort_by_key(|res_id| { - let type_count = tracker.get_type_count(res_id, slot.shift_type()); + let type_count = tracker.current_shift_type_workload(res_id, slot.shift_type()); let workload = tracker.current_workload(res_id); (type_count, workload) }); @@ -162,7 +162,7 @@ impl Scheduler { schedule: &MonthlySchedule, tracker: &WorkloadTracker, ) -> Vec { - let is_holiday_slot = self.config.is_holiday_or_weekend_slot(slot.day.0); + let is_holiday_slot = self.config.is_holiday_or_weekend_slot(slot); let other_resident_id = slot .other_position() .and_then(|partner_slot| schedule.get_resident_id(&partner_slot)); @@ -190,65 +190,9 @@ impl Scheduler { && r.allowed_types.contains(&slot.shift_type()) && !tracker.reached_workload_limit(&self.bounds, &r.id) && (!is_holiday_slot || !tracker.reached_holiday_limit(&self.bounds, &r.id)) - && !tracker.reached_shift_type_limit(&self.bounds, &r.id, &slot) + && !tracker.reached_shift_type_limit(&self.bounds, &r.id, slot.shift_type()) }) .map(|r| r.id) .collect() } } - -#[cfg(test)] -mod tests { - use rstest::{fixture, rstest}; - - use crate::{ - config::UserConfig, - resident::Resident, - schedule::MonthlySchedule, - scheduler::Scheduler, - workload::{WorkloadBounds, WorkloadTracker}, - }; - - #[fixture] - fn schedule() -> MonthlySchedule { - MonthlySchedule::new() - } - - #[fixture] - fn config() -> UserConfig { - UserConfig::default().with_residents(vec![ - Resident::new(1, "R1"), - Resident::new(2, "R2"), - Resident::new(3, "R3"), - Resident::new(4, "R4"), - Resident::new(5, "R5"), - Resident::new(6, "R6"), - ]) - } - - #[fixture] - fn bounds(config: UserConfig) -> WorkloadBounds { - WorkloadBounds::new_with_config(&config) - } - - #[fixture] - fn scheduler(config: UserConfig, bounds: WorkloadBounds) -> Scheduler { - Scheduler::new(config, bounds) - } - - #[fixture] - fn tracker() -> WorkloadTracker { - WorkloadTracker::default() - } - - #[rstest] - fn test_search( - mut schedule: MonthlySchedule, - mut tracker: WorkloadTracker, - scheduler: Scheduler, - ) { - let solved = scheduler.run(&mut schedule, &mut tracker); - assert!(solved.is_ok()); - assert!(solved.unwrap()); - } -} diff --git a/src-tauri/src/workload.rs b/src-tauri/src/workload.rs index 4fde08a..6638fba 100644 --- a/src-tauri/src/workload.rs +++ b/src-tauri/src/workload.rs @@ -104,7 +104,7 @@ impl WorkloadBounds { pub struct WorkloadTracker { total_counts: HashMap, type_counts: HashMap<(ResidentId, ShiftType), u8>, - holidays: HashMap, + holiday_counts: HashMap, } impl WorkloadTracker { @@ -115,8 +115,8 @@ impl WorkloadTracker { .entry((r_id, slot.shift_type())) .or_insert(0) += 1; - if config.is_holiday_or_weekend_slot(slot.day.0) { - *self.holidays.entry(r_id).or_insert(0) += 1; + if config.is_holiday_or_weekend_slot(slot) { + *self.holiday_counts.entry(r_id).or_insert(0) += 1; } } @@ -129,8 +129,8 @@ impl WorkloadTracker { *count = count.saturating_sub(1); } - if config.is_holiday_or_weekend_slot(slot.day.0) { - if let Some(count) = self.holidays.get_mut(&r_id) { + if config.is_holiday_or_weekend_slot(slot) { + if let Some(count) = self.holiday_counts.get_mut(&r_id) { *count = count.saturating_sub(1); } } @@ -141,7 +141,11 @@ impl WorkloadTracker { } pub fn current_holiday_workload(&self, r_id: &ResidentId) -> u8 { - *self.holidays.get(r_id).unwrap_or(&0) + *self.holiday_counts.get(r_id).unwrap_or(&0) + } + + pub fn current_shift_type_workload(&self, r_id: &ResidentId, shift_type: ShiftType) -> u8 { + *self.type_counts.get(&(*r_id, shift_type)).unwrap_or(&0) } pub fn reached_workload_limit(&self, bounds: &WorkloadBounds, r_id: &ResidentId) -> bool { @@ -172,9 +176,8 @@ impl WorkloadTracker { &self, bounds: &WorkloadBounds, r_id: &ResidentId, - slot: &Slot, + shift_type: ShiftType, ) -> bool { - let shift_type = slot.shift_type(); let current_load = self.type_counts.get(&(*r_id, shift_type)).unwrap_or(&0); if let Some(&max) = bounds.max_by_shift_type.get(&(*r_id, shift_type)) { @@ -183,10 +186,6 @@ impl WorkloadTracker { false } - - pub fn get_type_count(&self, r_id: &ResidentId, shift_type: ShiftType) -> u8 { - *self.type_counts.get(&(*r_id, shift_type)).unwrap_or(&0) - } } #[cfg(test)] @@ -194,85 +193,23 @@ mod tests { use crate::{ config::UserConfig, fixtures::{complex_config, hard_config, minimal_config}, - resident::{Resident, ResidentId}, + resident::ResidentId, schedule::ShiftType, slot::{Day, ShiftPosition, Slot}, workload::{WorkloadBounds, WorkloadTracker}, }; - use rstest::{fixture, rstest}; + use rstest::rstest; - #[fixture] - fn config() -> UserConfig { - UserConfig::default().with_residents(vec![ - Resident::new(1, "R1").with_max_shifts(2), - Resident::new(2, "R2").with_max_shifts(2), - Resident::new(3, "R3").with_reduced_load(), - Resident::new(4, "R4"), - Resident::new(5, "R5"), - ]) - } - - #[fixture] - fn tracker() -> WorkloadTracker { - WorkloadTracker::default() - } + // Testing WorkloadBounds #[rstest] - fn test_max_workloads(config: UserConfig) { - let bounds = WorkloadBounds::new_with_config(&config); - assert_eq!(2, bounds.max_workloads[&ResidentId(1)]); - assert_eq!(2, bounds.max_workloads[&ResidentId(2)]); - assert_eq!(13, bounds.max_workloads[&ResidentId(3)]); - assert_eq!(14, bounds.max_workloads[&ResidentId(4)]); - assert_eq!(14, bounds.max_workloads[&ResidentId(5)]); - } - - #[rstest] - fn test_reached_workload_limit(mut tracker: WorkloadTracker, config: UserConfig) { - let r_id = ResidentId(1); - let mut bounds = WorkloadBounds::default(); - bounds.max_workloads.insert(r_id, 1); - - let slot_1 = Slot::new(Day(1), ShiftPosition::First); - let slot_2 = Slot::new(Day(2), ShiftPosition::First); - - assert!(!tracker.reached_workload_limit(&bounds, &r_id,)); - - tracker.insert(r_id, &config, slot_1); - assert!(tracker.reached_workload_limit(&bounds, &r_id,)); - - tracker.insert(r_id, &config, slot_2); - assert!(tracker.reached_workload_limit(&bounds, &r_id,)); - } - - #[rstest] - fn test_reached_holiday_limit(mut tracker: WorkloadTracker, config: UserConfig) { - let r_id = ResidentId(1); - let mut bounds = WorkloadBounds::default(); - bounds.max_holiday_shifts.insert(r_id, 1); - - let sat = Slot::new(Day(11), ShiftPosition::First); - let sun = Slot::new(Day(12), ShiftPosition::First); - - assert!(!tracker.reached_holiday_limit(&bounds, &r_id)); - - tracker.insert(r_id, &config, sat); - assert!(tracker.reached_holiday_limit(&bounds, &r_id)); - - tracker.insert(r_id, &config, sun); - assert!(tracker.reached_holiday_limit(&bounds, &r_id)); - } - - #[rstest] - fn test_backtracking_accuracy(mut tracker: WorkloadTracker, config: UserConfig) { - let r_id = ResidentId(1); - let slot = Slot::new(Day(1), ShiftPosition::First); - - tracker.insert(r_id, &config, slot); - assert_eq!(tracker.current_workload(&r_id), 1); - - tracker.remove(r_id, &config, slot); - assert_eq!(tracker.current_workload(&r_id), 0); + fn test_max_workloads(minimal_config: UserConfig) { + let bounds = WorkloadBounds::new_with_config(&minimal_config); + assert_eq!(9, bounds.max_workloads[&ResidentId(1)]); + assert_eq!(9, bounds.max_workloads[&ResidentId(2)]); + assert_eq!(9, bounds.max_workloads[&ResidentId(3)]); + assert_eq!(9, bounds.max_workloads[&ResidentId(4)]); + assert_eq!(9, bounds.max_workloads[&ResidentId(5)]); } #[rstest] @@ -468,4 +405,88 @@ mod tests { assert_eq!(5, *m.get(&(ResidentId(8), ShiftType::OpenSecond)).unwrap()); assert_eq!(0, *m.get(&(ResidentId(8), ShiftType::Closed)).unwrap()); } + + // Testing WorkloadTracker + + #[rstest] + fn test_reached_workload_limit(minimal_config: UserConfig) { + let mut tracker = WorkloadTracker::default(); + let r_id = ResidentId(1); + let mut bounds = WorkloadBounds::default(); + bounds.max_workloads.insert(r_id, 1); + let slot_1 = Slot::new(Day(1), ShiftPosition::First); + let slot_2 = Slot::new(Day(2), ShiftPosition::First); + + assert!(!tracker.reached_workload_limit(&bounds, &r_id)); + tracker.insert(r_id, &minimal_config, slot_1); + assert!(tracker.reached_workload_limit(&bounds, &r_id)); + tracker.insert(r_id, &minimal_config, slot_2); + assert!(tracker.reached_workload_limit(&bounds, &r_id)); + } + + #[rstest] + fn test_reached_holiday_limit(minimal_config: UserConfig) { + let mut tracker = WorkloadTracker::default(); + let r_id = ResidentId(1); + let mut bounds = WorkloadBounds::default(); + bounds.max_holiday_shifts.insert(r_id, 1); + let sat = Slot::new(Day(11), ShiftPosition::First); + let sun = Slot::new(Day(12), ShiftPosition::First); + + assert!(!tracker.reached_holiday_limit(&bounds, &r_id)); + tracker.insert(r_id, &minimal_config, sat); + assert!(tracker.reached_holiday_limit(&bounds, &r_id)); + tracker.insert(r_id, &minimal_config, sun); + assert!(tracker.reached_holiday_limit(&bounds, &r_id)); + } + + #[rstest] + fn test_reached_shift_type_limit(minimal_config: UserConfig) { + let mut tracker = WorkloadTracker::default(); + let r_id = ResidentId(1); + let mut bounds = WorkloadBounds::default(); + bounds + .max_by_shift_type + .insert((r_id, ShiftType::OpenFirst), 1); + let slot_1 = Slot::new(Day(1), ShiftPosition::First); + let slot_2 = Slot::new(Day(3), ShiftPosition::First); + let open_first = ShiftType::OpenFirst; + + assert!(!tracker.reached_shift_type_limit(&bounds, &r_id, open_first)); + tracker.insert(r_id, &minimal_config, slot_1); + assert!(tracker.reached_shift_type_limit(&bounds, &r_id, open_first)); + tracker.insert(r_id, &minimal_config, slot_2); + assert!(tracker.reached_shift_type_limit(&bounds, &r_id, open_first)); + } + + #[rstest] + fn test_backtracking_state(minimal_config: UserConfig) { + let mut tracker = WorkloadTracker::default(); + let r_id = ResidentId(1); + let sat = Slot::new(Day(11), ShiftPosition::First); + let sun = Slot::new(Day(12), ShiftPosition::First); + let open_first = ShiftType::OpenFirst; + let closed = ShiftType::Closed; + + tracker.insert(r_id, &minimal_config, sat); + assert_eq!(1, tracker.current_workload(&r_id)); + assert_eq!(1, tracker.current_holiday_workload(&r_id)); + assert_eq!(1, tracker.current_shift_type_workload(&r_id, open_first)); + tracker.insert(r_id, &minimal_config, sun); + assert_eq!(2, tracker.current_workload(&r_id)); + assert_eq!(2, tracker.current_holiday_workload(&r_id)); + assert_eq!(1, tracker.current_shift_type_workload(&r_id, open_first)); + assert_eq!(1, tracker.current_shift_type_workload(&r_id, closed)); + + tracker.remove(r_id, &minimal_config, sun); + assert_eq!(1, tracker.current_workload(&r_id)); + assert_eq!(1, tracker.current_holiday_workload(&r_id)); + assert_eq!(1, tracker.current_shift_type_workload(&r_id, open_first)); + assert_eq!(0, tracker.current_shift_type_workload(&r_id, closed)); + tracker.remove(r_id, &minimal_config, sat); + assert_eq!(0, tracker.current_workload(&r_id)); + assert_eq!(0, tracker.current_holiday_workload(&r_id)); + assert_eq!(0, tracker.current_shift_type_workload(&r_id, open_first)); + assert_eq!(0, tracker.current_shift_type_workload(&r_id, closed)); + } } diff --git a/src-tauri/tests/common/mod.rs b/src-tauri/tests/common/mod.rs new file mode 100644 index 0000000..6a0b2a4 --- /dev/null +++ b/src-tauri/tests/common/mod.rs @@ -0,0 +1,58 @@ +use rota_lib::{ + config::{ToxicPair, UserConfig}, + schedule::MonthlySchedule, + slot::{Day, ShiftPosition, Slot}, + workload::{WorkloadBounds, WorkloadTracker}, +}; + +pub 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 r in current { + assert!(!previous.contains(&r)); + } + } + + 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, r_id) in &schedule.0 { + let r = config + .residents + .iter() + .find(|r| &r.id == r_id) + .expect("Resident not found"); + assert!(r.allowed_types.contains(&slot.shift_type())); + assert!(!r.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, "workload: {}, max: {}", workload, max); + } +} diff --git a/src-tauri/tests/integration.rs b/src-tauri/tests/integration.rs index 952d11e..bd4bfe0 100644 --- a/src-tauri/tests/integration.rs +++ b/src-tauri/tests/integration.rs @@ -1,14 +1,16 @@ +mod common; + #[cfg(test)] mod integration_tests { + use crate::common::validate_all_constraints; use rota_lib::{ - config::{ToxicPair, UserConfig}, + config::UserConfig, fixtures::{ complex_config, hard_config, manual_shifts_heavy_config, maximal_config, minimal_config, }, schedule::MonthlySchedule, scheduler::Scheduler, - slot::{Day, ShiftPosition, Slot}, - workload::{WorkloadBounds, WorkloadTracker}, + workload::WorkloadTracker, }; use rstest::rstest; @@ -91,55 +93,18 @@ mod integration_tests { Ok(()) } - fn validate_all_constraints( - schedule: &MonthlySchedule, - tracker: &WorkloadTracker, - config: &UserConfig, - ) { - assert_eq!(schedule.0.len() as u8, config.total_slots); + #[rstest] + fn test_export_pipeline(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)?); - 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 r in current { - assert!(!previous.contains(&r)); - } - } + schedule.export_as_docx(&minimal_config)?; - 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, r_id) in &schedule.0 { - let r = config - .residents - .iter() - .find(|r| &r.id == r_id) - .expect("Resident not found"); - assert!(r.allowed_types.contains(&slot.shift_type())); - assert!(!r.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, "workload: {}, max: {}", workload, max); - } + let metadata = std::fs::metadata("rota.docx")?; + assert!(metadata.len() > 0); + std::fs::remove_file("rota.docx")?; + Ok(()) } }