Reorganize integration tests, simplify fn signatures

This commit is contained in:
2026-02-22 13:01:28 +02:00
parent a41d1cd469
commit 76d308351a
8 changed files with 270 additions and 344 deletions

View File

@@ -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));
}
}

View File

@@ -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(())
}
}

View File

@@ -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")

View File

@@ -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))
}
}

View File

@@ -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());
}
}

View File

@@ -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));
}
}

View 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);
}
}

View File

@@ -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(())
}
}