Reorganize integration tests, simplify fn signatures
This commit is contained in:
@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use crate::{
|
use crate::{
|
||||||
resident::{Resident, ResidentDTO, ResidentId},
|
resident::{Resident, ResidentDTO, ResidentId},
|
||||||
schedule::ShiftType,
|
schedule::ShiftType,
|
||||||
slot::Day,
|
slot::{Day, Slot},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MONTH: u8 = 4;
|
const MONTH: u8 = 4;
|
||||||
@@ -58,6 +58,32 @@ pub struct UserConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
pub fn with_holidays(mut self, holidays: Vec<u8>) -> Self {
|
||||||
self.holidays = holidays;
|
self.holidays = holidays;
|
||||||
self.total_holiday_slots = self.total_holiday_slots();
|
self.total_holiday_slots = self.total_holiday_slots();
|
||||||
@@ -80,15 +106,18 @@ impl UserConfig {
|
|||||||
|
|
||||||
fn total_holiday_slots(&self) -> u8 {
|
fn total_holiday_slots(&self) -> u8 {
|
||||||
(1..=self.total_days)
|
(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 })
|
.map(|d| if Day(d).is_open_shift() { 2 } else { 1 })
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_holiday_or_weekend_slot(&self, day: u8) -> bool {
|
pub fn is_holiday_or_weekend(&self, day: Day) -> bool {
|
||||||
let day = Day(day);
|
let month = self.month.number_from_month();
|
||||||
day.is_weekend(self.month.number_from_month(), self.year)
|
day.is_weekend(month, self.year) || self.holidays.contains(&(day.0))
|
||||||
|| 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> {
|
pub fn get_initial_supply(&self) -> HashMap<ShiftType, u8> {
|
||||||
@@ -174,7 +203,12 @@ impl TryFrom<UserConfigDTO> for UserConfig {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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;
|
use rstest::rstest;
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
@@ -185,4 +219,25 @@ mod tests {
|
|||||||
assert_eq!(15, *supply.get(&ShiftType::OpenSecond).unwrap());
|
assert_eq!(15, *supply.get(&ShiftType::OpenSecond).unwrap());
|
||||||
assert_eq!(15, *supply.get(&ShiftType::Closed).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(())
|
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},
|
slot::{Day, ShiftPosition, Slot},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use chrono::Month;
|
||||||
use rstest::fixture;
|
use rstest::fixture;
|
||||||
|
|
||||||
#[fixture]
|
#[fixture]
|
||||||
pub fn minimal_config() -> UserConfig {
|
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(1, "R1"),
|
||||||
Resident::new(2, "R2"),
|
Resident::new(2, "R2"),
|
||||||
Resident::new(3, "R3"),
|
Resident::new(3, "R3"),
|
||||||
@@ -20,7 +21,7 @@ pub fn minimal_config() -> UserConfig {
|
|||||||
|
|
||||||
#[fixture]
|
#[fixture]
|
||||||
pub fn maximal_config() -> UserConfig {
|
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_holidays(vec![2, 3, 10, 11, 12, 25])
|
||||||
.with_residents(vec![
|
.with_residents(vec![
|
||||||
Resident::new(1, "R1").with_max_shifts(3),
|
Resident::new(1, "R1").with_max_shifts(3),
|
||||||
@@ -30,21 +31,23 @@ pub fn maximal_config() -> UserConfig {
|
|||||||
Resident::new(5, "R5")
|
Resident::new(5, "R5")
|
||||||
.with_allowed_types(vec![ShiftType::OpenFirst, ShiftType::OpenSecond]),
|
.with_allowed_types(vec![ShiftType::OpenFirst, ShiftType::OpenSecond]),
|
||||||
Resident::new(6, "R6").with_negative_shifts(vec![Day(5), Day(15), Day(25)]),
|
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(8, "R8"),
|
||||||
Resident::new(9, "R9"),
|
Resident::new(9, "R9"),
|
||||||
Resident::new(10, "R10"),
|
Resident::new(10, "R10").with_reduced_load(),
|
||||||
])
|
])
|
||||||
.with_toxic_pairs(vec![
|
.with_toxic_pairs(vec![
|
||||||
ToxicPair::new(1, 2),
|
ToxicPair::new(1, 2),
|
||||||
ToxicPair::new(3, 4),
|
ToxicPair::new(3, 4),
|
||||||
|
ToxicPair::new(3, 5),
|
||||||
ToxicPair::new(7, 8),
|
ToxicPair::new(7, 8),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
#[fixture]
|
#[fixture]
|
||||||
pub fn manual_shifts_heavy_config() -> UserConfig {
|
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![
|
Resident::new(1, "R1").with_manual_shifts(vec![
|
||||||
Slot::new(Day(1), ShiftPosition::First),
|
Slot::new(Day(1), ShiftPosition::First),
|
||||||
Slot::new(Day(3), ShiftPosition::First),
|
Slot::new(Day(3), ShiftPosition::First),
|
||||||
@@ -63,8 +66,8 @@ pub fn manual_shifts_heavy_config() -> UserConfig {
|
|||||||
|
|
||||||
#[fixture]
|
#[fixture]
|
||||||
pub fn complex_config() -> UserConfig {
|
pub fn complex_config() -> UserConfig {
|
||||||
UserConfig::default()
|
UserConfig::new(Month::April.number_from_month() as u8, 2026)
|
||||||
.with_holidays(vec![5, 12, 19, 26])
|
.with_holidays(vec![5, 10, 12, 19])
|
||||||
.with_residents(vec![
|
.with_residents(vec![
|
||||||
Resident::new(1, "R1")
|
Resident::new(1, "R1")
|
||||||
.with_max_shifts(3)
|
.with_max_shifts(3)
|
||||||
@@ -93,7 +96,7 @@ pub fn complex_config() -> UserConfig {
|
|||||||
|
|
||||||
#[fixture]
|
#[fixture]
|
||||||
pub fn hard_config() -> UserConfig {
|
pub fn hard_config() -> UserConfig {
|
||||||
UserConfig::default()
|
UserConfig::new(Month::April.number_from_month() as u8, 2026)
|
||||||
.with_holidays(vec![25])
|
.with_holidays(vec![25])
|
||||||
.with_residents(vec![
|
.with_residents(vec![
|
||||||
Resident::new(1, "R1")
|
Resident::new(1, "R1")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use serde::{ser::SerializeMap, Deserialize, Serialize};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{ToxicPair, UserConfig},
|
config::UserConfig,
|
||||||
resident::ResidentId,
|
resident::ResidentId,
|
||||||
slot::{weekday_to_greek, Day, ShiftPosition, Slot},
|
slot::{weekday_to_greek, Day, ShiftPosition, Slot},
|
||||||
workload::WorkloadTracker,
|
workload::WorkloadTracker,
|
||||||
@@ -75,24 +75,6 @@ impl MonthlySchedule {
|
|||||||
|| self.get_resident_id(&second) == Some(&res_id)
|
|| 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 {
|
pub fn pretty_print(&self, config: &UserConfig) -> String {
|
||||||
let mut sorted: Vec<_> = self.0.iter().collect();
|
let mut sorted: Vec<_> = self.0.iter().collect();
|
||||||
sorted.sort_by_key(|(slot, _)| (slot.day, slot.position));
|
sorted.sort_by_key(|(slot, _)| (slot.day, slot.position));
|
||||||
@@ -135,9 +117,9 @@ impl MonthlySchedule {
|
|||||||
|
|
||||||
for r in residents {
|
for r in residents {
|
||||||
let total = tracker.current_workload(&r.id);
|
let total = tracker.current_workload(&r.id);
|
||||||
let o1 = tracker.get_type_count(&r.id, ShiftType::OpenFirst);
|
let o1 = tracker.current_shift_type_workload(&r.id, ShiftType::OpenFirst);
|
||||||
let o2 = tracker.get_type_count(&r.id, ShiftType::OpenSecond);
|
let o2 = tracker.current_shift_type_workload(&r.id, ShiftType::OpenSecond);
|
||||||
let cl = tracker.get_type_count(&r.id, ShiftType::Closed);
|
let cl = tracker.current_shift_type_workload(&r.id, ShiftType::Closed);
|
||||||
let holiday = tracker.current_holiday_workload(&r.id);
|
let holiday = tracker.current_holiday_workload(&r.id);
|
||||||
|
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
@@ -178,39 +160,18 @@ pub enum ShiftType {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use rstest::{fixture, rstest};
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{ToxicPair, UserConfig},
|
|
||||||
resident::{Resident, ResidentId},
|
resident::{Resident, ResidentId},
|
||||||
schedule::{Day, MonthlySchedule, Slot},
|
schedule::{Day, MonthlySchedule, Slot},
|
||||||
slot::ShiftPosition,
|
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]
|
#[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_1 = Slot::new(Day(1), ShiftPosition::First);
|
||||||
let slot_2 = Slot::new(Day(1), ShiftPosition::Second);
|
let slot_2 = Slot::new(Day(1), ShiftPosition::Second);
|
||||||
|
|
||||||
@@ -221,7 +182,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[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);
|
let slot_1 = Slot::new(Day(1), ShiftPosition::First);
|
||||||
|
|
||||||
schedule.insert(slot_1, resident.id);
|
schedule.insert(slot_1, resident.id);
|
||||||
@@ -231,7 +194,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[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_1 = Slot::new(Day(1), ShiftPosition::First);
|
||||||
let slot_2 = Slot::new(Day(1), ShiftPosition::Second);
|
let slot_2 = Slot::new(Day(1), ShiftPosition::Second);
|
||||||
let slot_3 = Slot::new(Day(2), ShiftPosition::First);
|
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_2));
|
||||||
assert!(schedule.has_resident_in_consecutive_days(&slot_3));
|
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);
|
let mut valid_resident_ids = self.valid_residents(slot, schedule, tracker);
|
||||||
valid_resident_ids.shuffle(&mut rng);
|
valid_resident_ids.shuffle(&mut rng);
|
||||||
valid_resident_ids.sort_by_key(|res_id| {
|
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);
|
let workload = tracker.current_workload(res_id);
|
||||||
(type_count, workload)
|
(type_count, workload)
|
||||||
});
|
});
|
||||||
@@ -162,7 +162,7 @@ impl Scheduler {
|
|||||||
schedule: &MonthlySchedule,
|
schedule: &MonthlySchedule,
|
||||||
tracker: &WorkloadTracker,
|
tracker: &WorkloadTracker,
|
||||||
) -> Vec<ResidentId> {
|
) -> 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
|
let other_resident_id = slot
|
||||||
.other_position()
|
.other_position()
|
||||||
.and_then(|partner_slot| schedule.get_resident_id(&partner_slot));
|
.and_then(|partner_slot| schedule.get_resident_id(&partner_slot));
|
||||||
@@ -190,65 +190,9 @@ impl Scheduler {
|
|||||||
&& r.allowed_types.contains(&slot.shift_type())
|
&& r.allowed_types.contains(&slot.shift_type())
|
||||||
&& !tracker.reached_workload_limit(&self.bounds, &r.id)
|
&& !tracker.reached_workload_limit(&self.bounds, &r.id)
|
||||||
&& (!is_holiday_slot || !tracker.reached_holiday_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)
|
.map(|r| r.id)
|
||||||
.collect()
|
.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 {
|
pub struct WorkloadTracker {
|
||||||
total_counts: HashMap<ResidentId, u8>,
|
total_counts: HashMap<ResidentId, u8>,
|
||||||
type_counts: HashMap<(ResidentId, ShiftType), u8>,
|
type_counts: HashMap<(ResidentId, ShiftType), u8>,
|
||||||
holidays: HashMap<ResidentId, u8>,
|
holiday_counts: HashMap<ResidentId, u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkloadTracker {
|
impl WorkloadTracker {
|
||||||
@@ -115,8 +115,8 @@ impl WorkloadTracker {
|
|||||||
.entry((r_id, slot.shift_type()))
|
.entry((r_id, slot.shift_type()))
|
||||||
.or_insert(0) += 1;
|
.or_insert(0) += 1;
|
||||||
|
|
||||||
if config.is_holiday_or_weekend_slot(slot.day.0) {
|
if config.is_holiday_or_weekend_slot(slot) {
|
||||||
*self.holidays.entry(r_id).or_insert(0) += 1;
|
*self.holiday_counts.entry(r_id).or_insert(0) += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,8 +129,8 @@ impl WorkloadTracker {
|
|||||||
*count = count.saturating_sub(1);
|
*count = count.saturating_sub(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.is_holiday_or_weekend_slot(slot.day.0) {
|
if config.is_holiday_or_weekend_slot(slot) {
|
||||||
if let Some(count) = self.holidays.get_mut(&r_id) {
|
if let Some(count) = self.holiday_counts.get_mut(&r_id) {
|
||||||
*count = count.saturating_sub(1);
|
*count = count.saturating_sub(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,7 +141,11 @@ impl WorkloadTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_holiday_workload(&self, r_id: &ResidentId) -> u8 {
|
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 {
|
pub fn reached_workload_limit(&self, bounds: &WorkloadBounds, r_id: &ResidentId) -> bool {
|
||||||
@@ -172,9 +176,8 @@ impl WorkloadTracker {
|
|||||||
&self,
|
&self,
|
||||||
bounds: &WorkloadBounds,
|
bounds: &WorkloadBounds,
|
||||||
r_id: &ResidentId,
|
r_id: &ResidentId,
|
||||||
slot: &Slot,
|
shift_type: ShiftType,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let shift_type = slot.shift_type();
|
|
||||||
let current_load = self.type_counts.get(&(*r_id, shift_type)).unwrap_or(&0);
|
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)) {
|
if let Some(&max) = bounds.max_by_shift_type.get(&(*r_id, shift_type)) {
|
||||||
@@ -183,10 +186,6 @@ impl WorkloadTracker {
|
|||||||
|
|
||||||
false
|
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)]
|
#[cfg(test)]
|
||||||
@@ -194,85 +193,23 @@ mod tests {
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::UserConfig,
|
config::UserConfig,
|
||||||
fixtures::{complex_config, hard_config, minimal_config},
|
fixtures::{complex_config, hard_config, minimal_config},
|
||||||
resident::{Resident, ResidentId},
|
resident::ResidentId,
|
||||||
schedule::ShiftType,
|
schedule::ShiftType,
|
||||||
slot::{Day, ShiftPosition, Slot},
|
slot::{Day, ShiftPosition, Slot},
|
||||||
workload::{WorkloadBounds, WorkloadTracker},
|
workload::{WorkloadBounds, WorkloadTracker},
|
||||||
};
|
};
|
||||||
use rstest::{fixture, rstest};
|
use rstest::rstest;
|
||||||
|
|
||||||
#[fixture]
|
// Testing WorkloadBounds
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
fn test_max_workloads(config: UserConfig) {
|
fn test_max_workloads(minimal_config: UserConfig) {
|
||||||
let bounds = WorkloadBounds::new_with_config(&config);
|
let bounds = WorkloadBounds::new_with_config(&minimal_config);
|
||||||
assert_eq!(2, bounds.max_workloads[&ResidentId(1)]);
|
assert_eq!(9, bounds.max_workloads[&ResidentId(1)]);
|
||||||
assert_eq!(2, bounds.max_workloads[&ResidentId(2)]);
|
assert_eq!(9, bounds.max_workloads[&ResidentId(2)]);
|
||||||
assert_eq!(13, bounds.max_workloads[&ResidentId(3)]);
|
assert_eq!(9, bounds.max_workloads[&ResidentId(3)]);
|
||||||
assert_eq!(14, bounds.max_workloads[&ResidentId(4)]);
|
assert_eq!(9, bounds.max_workloads[&ResidentId(4)]);
|
||||||
assert_eq!(14, bounds.max_workloads[&ResidentId(5)]);
|
assert_eq!(9, 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
@@ -468,4 +405,88 @@ mod tests {
|
|||||||
assert_eq!(5, *m.get(&(ResidentId(8), ShiftType::OpenSecond)).unwrap());
|
assert_eq!(5, *m.get(&(ResidentId(8), ShiftType::OpenSecond)).unwrap());
|
||||||
assert_eq!(0, *m.get(&(ResidentId(8), ShiftType::Closed)).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)]
|
#[cfg(test)]
|
||||||
mod integration_tests {
|
mod integration_tests {
|
||||||
|
use crate::common::validate_all_constraints;
|
||||||
use rota_lib::{
|
use rota_lib::{
|
||||||
config::{ToxicPair, UserConfig},
|
config::UserConfig,
|
||||||
fixtures::{
|
fixtures::{
|
||||||
complex_config, hard_config, manual_shifts_heavy_config, maximal_config, minimal_config,
|
complex_config, hard_config, manual_shifts_heavy_config, maximal_config, minimal_config,
|
||||||
},
|
},
|
||||||
schedule::MonthlySchedule,
|
schedule::MonthlySchedule,
|
||||||
scheduler::Scheduler,
|
scheduler::Scheduler,
|
||||||
slot::{Day, ShiftPosition, Slot},
|
workload::WorkloadTracker,
|
||||||
workload::{WorkloadBounds, WorkloadTracker},
|
|
||||||
};
|
};
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
@@ -91,55 +93,18 @@ mod integration_tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_all_constraints(
|
#[rstest]
|
||||||
schedule: &MonthlySchedule,
|
fn test_export_pipeline(minimal_config: UserConfig) -> anyhow::Result<()> {
|
||||||
tracker: &WorkloadTracker,
|
let mut schedule = MonthlySchedule::new();
|
||||||
config: &UserConfig,
|
let mut tracker = WorkloadTracker::default();
|
||||||
) {
|
let scheduler = Scheduler::new_with_config(minimal_config.clone());
|
||||||
assert_eq!(schedule.0.len() as u8, config.total_slots);
|
assert!(scheduler.run(&mut schedule, &mut tracker)?);
|
||||||
|
|
||||||
for d in 2..=config.total_days {
|
schedule.export_as_docx(&minimal_config)?;
|
||||||
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 metadata = std::fs::metadata("rota.docx")?;
|
||||||
let day = Day(d);
|
assert!(metadata.len() > 0);
|
||||||
if day.is_open_shift() {
|
std::fs::remove_file("rota.docx")?;
|
||||||
let r1 = schedule.get_resident_id(&Slot::new(day, ShiftPosition::First));
|
Ok(())
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user