From 3ecdc91802d869531a29041359723953638468e0 Mon Sep 17 00:00:00 2001 From: stefiosif Date: Sat, 28 Feb 2026 10:01:26 +0200 Subject: [PATCH] Expand integration suite to cover all months of 2026, minor refactorings --- justfile | 7 +++-- src-tauri/src/config.rs | 28 +++++++++++++++++- src-tauri/src/export.rs | 34 ++++++++++++++++----- src-tauri/src/fixtures.rs | 11 ++++--- src-tauri/src/lib.rs | 7 ----- src-tauri/src/schedule.rs | 20 ++----------- src-tauri/src/slot.rs | 54 ++++++++++++++-------------------- src-tauri/tests/integration.rs | 41 +++++++++++++++++++------- 8 files changed, 118 insertions(+), 84 deletions(-) diff --git a/justfile b/justfile index 8af4c07..f9b45f8 100644 --- a/justfile +++ b/justfile @@ -25,8 +25,11 @@ test-all: bench: cd {{tauri_path}} && cargo bench -# profile: -# cd {{tauri_path}} && cargo flamegraph +cov: + cd {{tauri_path}} && cargo llvm-cov run + +mutants: + cd {{tauri_path}} && cargo mutants clean: rm -rf node_modules diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index d6d78ea..b867144 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -104,6 +104,18 @@ impl UserConfig { self } + pub fn update_month(&mut self, month: u8) { + self.month = Month::try_from(month).unwrap(); + + self.total_days = self.month.num_days(self.year).unwrap(); + + self.total_slots = (1..=self.total_days) + .map(|d| if Day(d).is_open_shift() { 2 } else { 1 }) + .sum(); + + self.total_holiday_slots = self.total_holiday_slots() + } + fn total_holiday_slots(&self) -> u8 { (1..=self.total_days) .filter(|&d| self.is_holiday_or_weekend(Day(d))) @@ -212,7 +224,7 @@ impl TryFrom for UserConfig { #[cfg(test)] mod tests { use crate::{ - config::UserConfig, + config::{ToxicPair, UserConfig}, fixtures::complex_config, schedule::ShiftType, slot::{Day, ShiftPosition, Slot}, @@ -248,4 +260,18 @@ mod tests { assert!(complex_config.is_holiday_or_weekend_slot(sun)); assert!(complex_config.is_holiday_or_weekend_slot(manual_holiday)); } + + #[rstest] + fn test_toxic_pair_matches() { + let pair_1 = ToxicPair::new(1, 2); + let pair_1_r = ToxicPair::new(2, 1); + let pair_2 = ToxicPair::new(3, 1); + + assert!(pair_1.matches(&pair_1_r)); + assert!(pair_1_r.matches(&pair_1)); + assert!(!pair_1.matches(&pair_2)); + assert!(!pair_2.matches(&pair_1)); + assert!(!pair_1_r.matches(&pair_2)); + assert!(!pair_2.matches(&pair_1_r)); + } } diff --git a/src-tauri/src/export.rs b/src-tauri/src/export.rs index 687fe11..92f5b7a 100644 --- a/src-tauri/src/export.rs +++ b/src-tauri/src/export.rs @@ -6,7 +6,7 @@ use crate::{ config::UserConfig, errors::ExportError, schedule::MonthlySchedule, - slot::{month_to_greek, weekday_to_greek, Day, ShiftPosition, Slot}, + slot::{Day, ShiftPosition, Slot}, workload::WorkloadTracker, }; @@ -97,21 +97,21 @@ impl MonthlySchedule { let day = Day(d); let is_weekend = day.is_weekend(config.month.number_from_month(), config.year); let slot_first = Slot::new(Day(d), ShiftPosition::First); - let slot_first_res_id = self.get_resident_id(&slot_first); + let slot_first_r_id = self.get_resident_id(&slot_first); let res_name_1 = config .residents .iter() - .find(|r| Some(&r.id) == slot_first_res_id) + .find(|r| Some(&r.id) == slot_first_r_id) .map(|r| r.name.as_str()) .unwrap_or("-"); let res_name_2 = if day.is_open_shift() { let slot_second = Slot::new(Day(d), ShiftPosition::Second); - let slot_second_res_id = self.get_resident_id(&slot_second); + let slot_second_r_id = self.get_resident_id(&slot_second); config .residents .iter() - .find(|r| Some(&r.id) == slot_second_res_id) + .find(|r| Some(&r.id) == slot_second_r_id) .map(|r| r.name.as_str()) } else { None @@ -133,9 +133,9 @@ impl MonthlySchedule { ), TableCell::new().add_paragraph( Paragraph::new() - .add_run(make_run(weekday_to_greek( - Day(d).weekday(config.month.number_from_month(), config.year), - ))) + .add_run(make_run( + Day(d).weekday_to_greek(config.month.number_from_month(), config.year), + )) .fonts(RunFonts::new().ascii("Arial")), ), TableCell::new().add_paragraph( @@ -172,3 +172,21 @@ impl MonthlySchedule { Ok(()) } } + +fn month_to_greek(month: u32) -> &'static str { + match month { + 1 => "Ιανουάριος", + 2 => "Φεβρουάριος", + 3 => "Μάρτιος", + 4 => "Απρίλιος", + 5 => "Μάιος", + 6 => "Ιούνιος", + 7 => "Ιούλιος", + 8 => "Αύγουστος", + 9 => "Σεπτέμβριος", + 10 => "Οκτώβριος", + 11 => "Νοέμβριος", + 12 => "Δεκέμβριος", + _ => panic!("Unable to find translation for month {}", month), + } +} diff --git a/src-tauri/src/fixtures.rs b/src-tauri/src/fixtures.rs index 768118b..a208ded 100644 --- a/src-tauri/src/fixtures.rs +++ b/src-tauri/src/fixtures.rs @@ -5,12 +5,11 @@ use crate::{ slot::{Day, ShiftPosition, Slot}, }; -use chrono::Month; use rstest::fixture; #[fixture] pub fn minimal_config() -> UserConfig { - UserConfig::new(Month::April.number_from_month() as u8, 2026).with_residents(vec![ + UserConfig::default().with_residents(vec![ Resident::new(1, "R1"), Resident::new(2, "R2"), Resident::new(3, "R3"), @@ -21,7 +20,7 @@ pub fn minimal_config() -> UserConfig { #[fixture] pub fn maximal_config() -> UserConfig { - UserConfig::new(Month::April.number_from_month() as u8, 2026) + UserConfig::default() .with_holidays(vec![2, 3, 10, 11, 12, 25]) .with_residents(vec![ Resident::new(1, "R1").with_max_shifts(3), @@ -47,7 +46,7 @@ pub fn maximal_config() -> UserConfig { #[fixture] pub fn manual_shifts_heavy_config() -> UserConfig { - UserConfig::new(Month::April.number_from_month() as u8, 2026).with_residents(vec![ + 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), @@ -66,7 +65,7 @@ pub fn manual_shifts_heavy_config() -> UserConfig { #[fixture] pub fn complex_config() -> UserConfig { - UserConfig::new(Month::April.number_from_month() as u8, 2026) + UserConfig::default() .with_holidays(vec![5, 10, 12, 19]) .with_residents(vec![ Resident::new(1, "R1") @@ -96,7 +95,7 @@ pub fn complex_config() -> UserConfig { #[fixture] pub fn hard_config() -> UserConfig { - UserConfig::new(Month::April.number_from_month() as u8, 2026) + UserConfig::default() .with_holidays(vec![25]) .with_residents(vec![ Resident::new(1, "R1") diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 58335dc..f735125 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -107,9 +107,7 @@ pub fn run() { #[cfg(test)] mod tests { - use ctor::ctor; - use rstest::rstest; #[ctor] fn global_setup() { @@ -118,9 +116,4 @@ mod tests { .is_test(true) .try_init(); } - - #[rstest] - pub fn test_endpoints() { - // see how tauri mocks endpoint tests - } } diff --git a/src-tauri/src/schedule.rs b/src-tauri/src/schedule.rs index 160d0b5..2717f8c 100644 --- a/src-tauri/src/schedule.rs +++ b/src-tauri/src/schedule.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use crate::{ config::UserConfig, resident::ResidentId, - slot::{weekday_to_greek, Day, ShiftPosition, Slot}, + slot::{Day, ShiftPosition, Slot}, workload::WorkloadTracker, }; @@ -63,18 +63,6 @@ impl MonthlySchedule { .any(|s| self.get_resident_id(s) == self.get_resident_id(slot)) } - pub fn resident_worked_on_day(&self, day: Day, res_id: ResidentId) -> bool { - if day.0 < 1 { - return false; - } - - let first = Slot::new(day, ShiftPosition::First); - let second = Slot::new(day, ShiftPosition::Second); - - self.get_resident_id(&first) == Some(&res_id) - || self.get_resident_id(&second) == Some(&res_id) - } - 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)); @@ -91,10 +79,8 @@ impl MonthlySchedule { output.push_str(&format!( "Ημέρα {:2} - {:9} - {:11}: {},\n", slot.day.0, - weekday_to_greek( - slot.day - .weekday(config.month.number_from_month(), config.year) - ), + slot.day + .weekday_to_greek(config.month.number_from_month(), config.year), slot.shift_type_str(), res_name )); diff --git a/src-tauri/src/slot.rs b/src-tauri/src/slot.rs index 4a347e9..b4b3a32 100644 --- a/src-tauri/src/slot.rs +++ b/src-tauri/src/slot.rs @@ -23,6 +23,10 @@ impl Slot { self.day == Day(1) && self.position == ShiftPosition::First } + pub fn is_first_day(&self) -> bool { + self.day == Day(1) + } + pub fn is_open_first(&self) -> bool { self.is_open_shift() && self.position == ShiftPosition::First } @@ -160,9 +164,17 @@ impl Day { } } - pub fn weekday(&self, month: u32, year: i32) -> Weekday { + pub fn weekday_to_greek(&self, month: u32, year: i32) -> &'static str { let date = NaiveDate::from_ymd_opt(year, month, self.0 as u32).unwrap(); - date.weekday() + match date.weekday() { + Weekday::Mon => "Δευτέρα", + Weekday::Tue => "Τρίτη", + Weekday::Wed => "Τετάρτη", + Weekday::Thu => "Πέμπτη", + Weekday::Fri => "Παρασκευή", + Weekday::Sat => "Σάββατο", + Weekday::Sun => "Κυριακή", + } } } @@ -172,36 +184,6 @@ pub enum ShiftPosition { Second, } -pub fn weekday_to_greek(weekday: Weekday) -> &'static str { - match weekday { - Weekday::Mon => "Δευτέρα", - Weekday::Tue => "Τρίτη", - Weekday::Wed => "Τετάρτη", - Weekday::Thu => "Πέμπτη", - Weekday::Fri => "Παρασκευή", - Weekday::Sat => "Σάββατο", - Weekday::Sun => "Κυριακή", - } -} - -pub fn month_to_greek(month: u32) -> &'static str { - match month { - 1 => "Ιανουάριος", - 2 => "Φεβρουάριος", - 3 => "Μάρτιος", - 4 => "Απρίλιος", - 5 => "Μάιος", - 6 => "Ιούνιος", - 7 => "Ιούλιος", - 8 => "Αύγουστος", - 9 => "Σεπτέμβριος", - 10 => "Οκτώβριος", - 11 => "Νοέμβριος", - 12 => "Δεκέμβριος", - _ => panic!("Unable to find translation for month {}", month), - } -} - #[cfg(test)] mod tests { use rstest::rstest; @@ -225,6 +207,14 @@ mod tests { assert!(!slot_2.is_first()); assert!(!slot_3.is_first()); + assert!(slot_1.is_first_day()); + assert!(slot_2.is_first_day()); + assert!(!slot_3.is_first_day()); + + assert!(slot_1.is_open_first()); + assert!(!slot_2.is_open_first()); + assert!(!slot_3.is_open_first()); + assert!(!slot_1.is_open_second()); assert!(slot_2.is_open_second()); assert!(!slot_3.is_open_second()); diff --git a/src-tauri/tests/integration.rs b/src-tauri/tests/integration.rs index bd4bfe0..50dba3d 100644 --- a/src-tauri/tests/integration.rs +++ b/src-tauri/tests/integration.rs @@ -22,10 +22,14 @@ mod integration_tests { } #[rstest] - fn test_minimal_config(minimal_config: UserConfig) -> anyhow::Result<()> { + fn test_minimal_config( + #[values(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)] month_idx: u8, + mut minimal_config: UserConfig, + ) -> anyhow::Result<()> { + minimal_config.update_month(month_idx); + let scheduler = Scheduler::new_with_config(minimal_config.clone()); let mut schedule = MonthlySchedule::new(); let mut tracker = WorkloadTracker::default(); - let scheduler = Scheduler::new_with_config(minimal_config.clone()); let solved = scheduler.run(&mut schedule, &mut tracker)?; println!("{}", schedule.report(&minimal_config, &tracker)); @@ -36,10 +40,14 @@ mod integration_tests { } #[rstest] - fn test_maximal_config(maximal_config: UserConfig) -> anyhow::Result<()> { + fn test_maximal_config( + #[values(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)] month_idx: u8, + mut maximal_config: UserConfig, + ) -> anyhow::Result<()> { + maximal_config.update_month(month_idx); + let scheduler = Scheduler::new_with_config(maximal_config.clone()); let mut schedule = MonthlySchedule::new(); let mut tracker = WorkloadTracker::default(); - let scheduler = Scheduler::new_with_config(maximal_config.clone()); let solved = scheduler.run(&mut schedule, &mut tracker)?; println!("{}", schedule.report(&maximal_config, &tracker)); @@ -51,11 +59,13 @@ mod integration_tests { #[rstest] fn test_manual_shifts_heavy_config( - manual_shifts_heavy_config: UserConfig, + #[values(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)] month_idx: u8, + mut manual_shifts_heavy_config: UserConfig, ) -> anyhow::Result<()> { + manual_shifts_heavy_config.update_month(month_idx); + let scheduler = Scheduler::new_with_config(manual_shifts_heavy_config.clone()); let mut schedule = MonthlySchedule::new(); let mut tracker = WorkloadTracker::default(); - let scheduler = Scheduler::new_with_config(manual_shifts_heavy_config.clone()); let solved = scheduler.run(&mut schedule, &mut tracker)?; println!("{}", schedule.report(&manual_shifts_heavy_config, &tracker)); @@ -66,10 +76,14 @@ mod integration_tests { } #[rstest] - fn test_complex_config(complex_config: UserConfig) -> anyhow::Result<()> { + fn test_complex_config( + #[values(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)] month_idx: u8, + mut complex_config: UserConfig, + ) -> anyhow::Result<()> { + complex_config.update_month(month_idx); + let scheduler = Scheduler::new_with_config(complex_config.clone()); let mut schedule = MonthlySchedule::new(); let mut tracker = WorkloadTracker::default(); - let scheduler = Scheduler::new_with_config(complex_config.clone()); let solved = scheduler.run(&mut schedule, &mut tracker)?; println!("{}", schedule.report(&complex_config, &tracker)); @@ -80,10 +94,14 @@ mod integration_tests { } #[rstest] - fn test_hard_config(hard_config: UserConfig) -> anyhow::Result<()> { + fn test_hard_config( + #[values(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)] month_idx: u8, + mut hard_config: UserConfig, + ) -> anyhow::Result<()> { + hard_config.update_month(month_idx); + let scheduler = Scheduler::new_with_config(hard_config.clone()); let mut schedule = MonthlySchedule::new(); let mut tracker = WorkloadTracker::default(); - let scheduler = Scheduler::new_with_config(hard_config.clone()); let solved = scheduler.run(&mut schedule, &mut tracker)?; println!("{}", schedule.report(&hard_config, &tracker)); @@ -95,9 +113,9 @@ mod integration_tests { #[rstest] fn test_export_pipeline(minimal_config: UserConfig) -> anyhow::Result<()> { + let scheduler = Scheduler::new_with_config(minimal_config.clone()); 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)?); schedule.export_as_docx(&minimal_config)?; @@ -105,6 +123,7 @@ mod integration_tests { let metadata = std::fs::metadata("rota.docx")?; assert!(metadata.len() > 0); std::fs::remove_file("rota.docx")?; + Ok(()) } }