Reorganize integration tests, simplify fn signatures
This commit is contained in:
@@ -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<u8>) -> 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<ShiftType, u8> {
|
||||
@@ -174,7 +203,12 @@ impl TryFrom<UserConfigDTO> 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ResidentId> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ impl WorkloadBounds {
|
||||
pub struct WorkloadTracker {
|
||||
total_counts: HashMap<ResidentId, u8>,
|
||||
type_counts: HashMap<(ResidentId, ShiftType), u8>,
|
||||
holidays: HashMap<ResidentId, u8>,
|
||||
holiday_counts: HashMap<ResidentId, u8>,
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
58
src-tauri/tests/common/mod.rs
Normal file
58
src-tauri/tests/common/mod.rs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user