Improve error handling, logging

This commit is contained in:
2026-01-18 00:02:11 +02:00
parent 125ddc3117
commit 33de9720bf
11 changed files with 211 additions and 109 deletions

View File

@@ -6,5 +6,5 @@
# will have schema files for capabilities auto-completion # will have schema files for capabilities auto-completion
/gen/schemas /gen/schemas
# Ignore exported txt/doc files and the log file
schedule.* rota.*

1
src-tauri/Cargo.lock generated
View File

@@ -3222,6 +3222,7 @@ dependencies = [
name = "rota" name = "rota"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"chrono", "chrono",
"docx-rs", "docx-rs",
"itertools", "itertools",

View File

@@ -29,3 +29,4 @@ tauri-plugin-log = "2"
log = "0.4.29" log = "0.4.29"
rand = "0.9.2" rand = "0.9.2"
docx-rs = "0.4.18" docx-rs = "0.4.18"
anyhow = "1.0.100"

View File

@@ -1,3 +1,4 @@
use anyhow::Context;
use chrono::Month; use chrono::Month;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -6,6 +7,7 @@ use crate::{
slot::Day, slot::Day,
}; };
const MONTH: u8 = 2;
const YEAR: i32 = 2026; const YEAR: i32 = 2026;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -53,30 +55,6 @@ pub struct UserConfig {
} }
impl UserConfig { impl UserConfig {
pub fn new(month: u8) -> 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: 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();
@@ -113,7 +91,7 @@ impl UserConfig {
impl Default for UserConfig { impl Default for UserConfig {
fn default() -> Self { fn default() -> Self {
let month = Month::try_from(2).unwrap(); let month = Month::try_from(MONTH).unwrap();
let total_days = month.num_days(YEAR).unwrap(); let total_days = month.num_days(YEAR).unwrap();
@@ -139,11 +117,13 @@ impl Default for UserConfig {
} }
} }
impl From<UserConfigDTO> for UserConfig { impl TryFrom<UserConfigDTO> for UserConfig {
fn from(value: UserConfigDTO) -> Self { type Error = anyhow::Error;
let month = Month::try_from(value.month).unwrap();
let total_days = month.num_days(YEAR).unwrap(); fn try_from(value: UserConfigDTO) -> Result<Self, Self::Error> {
let month = Month::try_from(value.month)?;
let total_days = month.num_days(value.year).context("Failed to parse")?;
let total_slots = (1..=total_days) let total_slots = (1..=total_days)
.map(|d| if Day(d).is_open_shift() { 2 } else { 1 }) .map(|d| if Day(d).is_open_shift() { 2 } else { 1 })
@@ -157,7 +137,7 @@ impl From<UserConfigDTO> for UserConfig {
.map(|d| if Day(d).is_open_shift() { 2 } else { 1 }) .map(|d| if Day(d).is_open_shift() { 2 } else { 1 })
.sum(); .sum();
Self { Ok(Self {
month, month,
year: value.year, year: value.year,
holidays: value.holidays, holidays: value.holidays,
@@ -170,6 +150,6 @@ impl From<UserConfigDTO> for UserConfig {
total_days, total_days,
total_slots, total_slots,
total_holiday_slots, total_holiday_slots,
} })
} }
} }

View File

@@ -1,5 +1,6 @@
use std::{fs::File, io::Write}; use std::{fs::File, io::Write};
use anyhow::Context;
use docx_rs::{Docx, Paragraph, Run, RunFonts, Table, TableCell, TableRow}; use docx_rs::{Docx, Paragraph, Run, RunFonts, Table, TableCell, TableRow};
use crate::{ use crate::{
@@ -16,45 +17,51 @@ pub enum FileType {
} }
pub trait Export { pub trait Export {
fn export(&self, file_type: FileType, config: &UserConfig, tracker: &WorkloadTracker); fn export(
&self,
file_type: FileType,
config: &UserConfig,
tracker: &WorkloadTracker,
) -> anyhow::Result<()>;
} }
impl Export for MonthlySchedule { impl Export for MonthlySchedule {
fn export(&self, file_type: FileType, config: &UserConfig, tracker: &WorkloadTracker) { fn export(
&self,
file_type: FileType,
config: &UserConfig,
tracker: &WorkloadTracker,
) -> anyhow::Result<()> {
match file_type { match file_type {
FileType::Txt => self.export_as_txt(config, tracker), FileType::Txt => self.export_as_txt(config, tracker)?,
FileType::Docx => self.export_as_doc(config), FileType::Docx => self.export_as_doc(config)?,
}; };
// TODO: make this env var from a config file? Option to change this in-app Ok(())
let env_path = "rota/me/";
println!(
"exported type {:?}. Saved at folder path {}",
file_type, env_path
);
} }
} }
impl MonthlySchedule { impl MonthlySchedule {
pub fn export_as_txt(&self, config: &UserConfig, tracker: &WorkloadTracker) -> String { pub fn export_as_txt(
let file = File::create("schedule.txt").unwrap(); &self,
config: &UserConfig,
tracker: &WorkloadTracker,
) -> anyhow::Result<()> {
let file = File::create("rota.txt")?;
let mut writer = std::io::BufWriter::new(file); let mut writer = std::io::BufWriter::new(file);
writer writer.write_all(self.pretty_print(config).as_bytes())?;
.write_all(self.pretty_print(config).as_bytes())
.expect("Failed to write schedule");
writer writer.write_all(self.report(config, tracker).as_bytes())?;
.write_all(self.report(config, tracker).as_bytes())
.expect("Failed to write report");
writer.flush().expect("Failed to flush buffer"); writer.flush()?;
"ok".to_string()
Ok(())
} }
pub fn export_as_doc(&self, config: &UserConfig) -> String { pub fn export_as_doc(&self, config: &UserConfig) -> anyhow::Result<()> {
let path = std::path::Path::new("./schedule.docx"); let path = std::path::Path::new("rota.docx");
let file = std::fs::File::create(path).unwrap(); let file = std::fs::File::create(path)?;
let header = Table::new(vec![ let header = Table::new(vec![
TableRow::new(vec![TableCell::new().add_paragraph( TableRow::new(vec![TableCell::new().add_paragraph(
@@ -101,7 +108,7 @@ impl MonthlySchedule {
.iter() .iter()
.find(|r| Some(&r.id) == slot_first_res_id) .find(|r| Some(&r.id) == slot_first_res_id)
.map(|r| r.name.as_str()) .map(|r| r.name.as_str())
.unwrap(); .unwrap_or("-");
let res_name_2 = if day.is_open_shift() { let res_name_2 = if day.is_open_shift() {
let slot_second = Slot::new(Day(d), ShiftPosition::Second); let slot_second = Slot::new(Day(d), ShiftPosition::Second);
@@ -153,18 +160,23 @@ impl MonthlySchedule {
doc = doc.add_table(residents_table); doc = doc.add_table(residents_table);
doc.build().pack(file).unwrap(); doc.build().pack(file)?;
"just a string".to_string() tauri_plugin_opener::open_path(path, None::<&str>)
.context("Created file but failed to open it")?;
Ok(())
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use anyhow::Ok;
use rstest::{fixture, rstest}; use rstest::{fixture, rstest};
use crate::{ use crate::{
config::UserConfig, config::UserConfig,
export::{Export, FileType},
resident::Resident, resident::Resident,
schedule::MonthlySchedule, schedule::MonthlySchedule,
scheduler::Scheduler, scheduler::Scheduler,
@@ -203,13 +215,29 @@ mod tests {
WorkloadTracker::default() 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] #[rstest]
pub fn test_export_as_doc( pub fn test_export_as_doc(
mut schedule: MonthlySchedule, mut schedule: MonthlySchedule,
mut tracker: WorkloadTracker, mut tracker: WorkloadTracker,
scheduler: Scheduler, scheduler: Scheduler,
) { ) -> anyhow::Result<()> {
scheduler.run(&mut schedule, &mut tracker); assert!(scheduler.run(&mut schedule, &mut tracker)?);
schedule.export_as_doc(&scheduler.config);
schedule.export(FileType::Docx, &scheduler.config, &tracker)?;
Ok(())
} }
} }

View File

@@ -1,11 +1,13 @@
use std::{env::home_dir, sync::Mutex}; use std::sync::Mutex;
use log::{error, info};
use crate::{ use crate::{
config::{UserConfig, UserConfigDTO}, config::{UserConfig, UserConfigDTO},
export::{Export, FileType}, export::{Export, FileType},
schedule::MonthlySchedule, schedule::MonthlySchedule,
scheduler::Scheduler, scheduler::Scheduler,
workload::{WorkloadBounds, WorkloadTracker}, workload::WorkloadTracker,
}; };
pub mod config; pub mod config;
@@ -14,6 +16,7 @@ pub mod resident;
pub mod schedule; pub mod schedule;
pub mod scheduler; pub mod scheduler;
pub mod slot; pub mod slot;
pub mod timer;
pub mod workload; pub mod workload;
struct AppState { struct AppState {
@@ -25,40 +28,61 @@ struct AppState {
/// information for the residents, the forbidden pairs the holidays /// information for the residents, the forbidden pairs the holidays
/// and the period of the schedule /// and the period of the schedule
#[tauri::command] #[tauri::command]
fn generate(config: UserConfigDTO, state: tauri::State<'_, AppState>) -> MonthlySchedule { fn generate(
let config = UserConfig::from(config); config: UserConfigDTO,
state: tauri::State<'_, AppState>,
) -> Result<MonthlySchedule, String> {
let mut schedule = MonthlySchedule::new(); let mut schedule = MonthlySchedule::new();
let mut tracker = WorkloadTracker::default(); let mut tracker = WorkloadTracker::default();
let bounds = WorkloadBounds::new_with_config(&config); let scheduler =
let scheduler = Scheduler::new(config, bounds); Scheduler::new_with_config(UserConfig::try_from(config).map_err(|e| e.to_string())?);
scheduler.run(&mut schedule, &mut tracker); scheduler
.run(&mut schedule, &mut tracker)
.inspect_err(|e| error!("{e}"))
.map_err(|e| e.to_string())?;
info!(
"Scheduler finished successfully in {}ms",
scheduler.timer.elapsed_in_ms()
);
let mut internal_schedule = state.schedule.lock().unwrap(); let mut internal_schedule = state.schedule.lock().unwrap();
*internal_schedule = schedule.clone();
let mut internal_tracker = state.tracker.lock().unwrap(); let mut internal_tracker = state.tracker.lock().unwrap();
*internal_schedule = schedule.clone();
*internal_tracker = tracker.clone(); *internal_tracker = tracker.clone();
schedule Ok(schedule)
} }
#[tauri::command] #[tauri::command]
fn export(config: UserConfigDTO, state: tauri::State<'_, AppState>) { fn export(config: UserConfigDTO, state: tauri::State<'_, AppState>) -> Result<(), String> {
let config = UserConfig::from(config); let config = UserConfig::try_from(config)
.inspect_err(|e| error!("{e}"))
.map_err(|e| e.to_string())?;
let schedule = state.schedule.lock().unwrap(); let schedule = state.schedule.lock().unwrap();
let tracker = state.tracker.lock().unwrap(); let tracker = state.tracker.lock().unwrap();
schedule.export(FileType::Docx, &config, &tracker);
schedule.export(FileType::Txt, &config, &tracker); schedule
.export(FileType::Docx, &config, &tracker)
.inspect_err(|e| error!("{e}"))
.map_err(|e| e.to_string())?;
schedule
.export(FileType::Txt, &config, &tracker)
.inspect_err(|e| error!("{e}"))
.map_err(|e| e.to_string())?;
let log_dir = std::env::current_dir().unwrap_or(std::path::PathBuf::from("."));
info!("Files exported at {}", log_dir.display());
Ok(())
} }
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
let log_dir = home_dir().unwrap().join(".rota_logs"); let log_dir = std::env::current_dir().unwrap_or(std::path::PathBuf::from("."));
if let Err(e) = std::fs::create_dir_all(&log_dir) {
eprintln!("Cannot create log folder: {}", e);
}
tauri::Builder::default() tauri::Builder::default()
.manage(AppState { .manage(AppState {
@@ -70,7 +94,7 @@ pub fn run() {
.targets([tauri_plugin_log::Target::new( .targets([tauri_plugin_log::Target::new(
tauri_plugin_log::TargetKind::Folder { tauri_plugin_log::TargetKind::Folder {
path: log_dir, path: log_dir,
file_name: Some("rota".to_string()), // Note: Plugin adds .log automatically file_name: Some("rota".to_string()),
}, },
)]) )])
.level(log::LevelFilter::Info) .level(log::LevelFilter::Info)
@@ -79,7 +103,7 @@ pub fn run() {
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![generate, export]) .invoke_handler(tauri::generate_handler![generate, export])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("Error while running tauri application");
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -119,7 +119,7 @@ impl MonthlySchedule {
.iter() .iter()
.find(|r| &r.id == res_id) .find(|r| &r.id == res_id)
.map(|r| r.name.as_str()) .map(|r| r.name.as_str())
.unwrap(); .unwrap_or("");
output.push_str(&format!( output.push_str(&format!(
"Ημέρα {:2} - {:9} - {:11}: {},\n", "Ημέρα {:2} - {:9} - {:11}: {},\n",

View File

@@ -3,6 +3,7 @@ use crate::{
resident::ResidentId, resident::ResidentId,
schedule::MonthlySchedule, schedule::MonthlySchedule,
slot::Slot, slot::Slot,
timer::Timer,
workload::{WorkloadBounds, WorkloadTracker}, workload::{WorkloadBounds, WorkloadTracker},
}; };
@@ -11,25 +12,40 @@ use rand::Rng;
pub struct Scheduler { pub struct Scheduler {
pub config: UserConfig, pub config: UserConfig,
pub bounds: WorkloadBounds, pub bounds: WorkloadBounds,
pub timer: Timer,
} }
impl Scheduler { impl Scheduler {
pub fn new(config: UserConfig, bounds: WorkloadBounds) -> Self { pub fn new(config: UserConfig, bounds: WorkloadBounds) -> Self {
Self { config, bounds } Self {
config,
bounds,
timer: Timer::default(),
}
} }
pub fn new_with_config(config: UserConfig) -> Self { pub fn new_with_config(config: UserConfig) -> Self {
let bounds = WorkloadBounds::new_with_config(&config); let bounds = WorkloadBounds::new_with_config(&config);
Self { config, bounds } Self {
config,
bounds,
timer: Timer::default(),
}
} }
pub fn run(&self, schedule: &mut MonthlySchedule, tracker: &mut WorkloadTracker) -> bool { pub fn run(
&self,
schedule: &mut MonthlySchedule,
tracker: &mut WorkloadTracker,
) -> anyhow::Result<bool> {
schedule.prefill(&self.config); schedule.prefill(&self.config);
for (slot, res_id) in schedule.0.iter() { for (slot, res_id) in schedule.0.iter() {
tracker.insert(*res_id, &self.config, *slot); tracker.insert(*res_id, &self.config, *slot);
} }
//TODO: add validation
self.search(schedule, tracker, Slot::default()) self.search(schedule, tracker, Slot::default())
} }
@@ -41,15 +57,19 @@ impl Scheduler {
schedule: &mut MonthlySchedule, schedule: &mut MonthlySchedule,
tracker: &mut WorkloadTracker, tracker: &mut WorkloadTracker,
slot: Slot, slot: Slot,
) -> bool { ) -> anyhow::Result<bool> {
if self.timer.limit_exceeded() {
anyhow::bail!("Time exceeded. Restrictions too tight");
}
if !slot.is_first() if !slot.is_first()
&& schedule.restrictions_violated(&slot.previous(), &self.config, &self.bounds, tracker) && schedule.restrictions_violated(&slot.previous(), &self.config, &self.bounds, tracker)
{ {
return false; return Ok(false);
} }
if slot.greater_than(self.config.total_days) { if slot.greater_than(self.config.total_days) {
return tracker.are_all_thresholds_met(&self.config, &self.bounds); return Ok(tracker.are_all_thresholds_met(&self.config, &self.bounds));
} }
if schedule.is_slot_manually_assigned(&slot) { if schedule.is_slot_manually_assigned(&slot) {
@@ -68,15 +88,15 @@ impl Scheduler {
schedule.insert(slot, id); schedule.insert(slot, id);
tracker.insert(id, &self.config, slot); tracker.insert(id, &self.config, slot);
if self.search(schedule, tracker, slot.next()) { if self.search(schedule, tracker, slot.next())? {
return true; return Ok(true);
} }
schedule.remove(slot); schedule.remove(slot);
tracker.remove(id, &self.config, slot); tracker.remove(id, &self.config, slot);
} }
false Ok(false)
} }
/// Return all valid residents for the current slot /// Return all valid residents for the current slot
@@ -150,7 +170,7 @@ mod tests {
mut tracker: WorkloadTracker, mut tracker: WorkloadTracker,
scheduler: Scheduler, scheduler: Scheduler,
) { ) {
assert!(scheduler.run(&mut schedule, &mut tracker)); assert!(scheduler.run(&mut schedule, &mut tracker).is_ok());
for d in 1..=scheduler.config.total_days { for d in 1..=scheduler.config.total_days {
let day = Day(d); let day = Day(d);

View File

@@ -139,10 +139,16 @@ impl Day {
} }
pub fn is_weekend(&self, month: u32, year: i32) -> bool { pub fn is_weekend(&self, month: u32, year: i32) -> bool {
let date = NaiveDate::from_ymd_opt(year, month, self.0 as u32).unwrap(); let date = NaiveDate::from_ymd_opt(year, month, self.0 as u32);
match date {
Some(date) => {
let weekday = date.weekday(); let weekday = date.weekday();
weekday == Weekday::Sat || weekday == Weekday::Sun weekday == Weekday::Sat || weekday == Weekday::Sun
} }
None => false,
}
}
pub fn weekday(&self, month: u32, year: i32) -> Weekday { pub fn weekday(&self, month: u32, year: i32) -> Weekday {
let date = NaiveDate::from_ymd_opt(year, month, self.0 as u32).unwrap(); let date = NaiveDate::from_ymd_opt(year, month, self.0 as u32).unwrap();

27
src-tauri/src/timer.rs Normal file
View File

@@ -0,0 +1,27 @@
use std::time::Instant;
pub const TIME_LIMIT_IN_MS: u128 = 100000;
pub struct Timer {
instant: Instant,
limit: u128,
}
impl Timer {
pub fn limit_exceeded(&self) -> bool {
self.instant.elapsed().as_millis() > self.limit
}
pub fn elapsed_in_ms(&self) -> u128 {
self.instant.elapsed().as_millis()
}
}
impl Default for Timer {
fn default() -> Self {
Self {
instant: std::time::Instant::now(),
limit: TIME_LIMIT_IN_MS,
}
}
}

View File

@@ -1,5 +1,6 @@
#[cfg(test)] #[cfg(test)]
mod integration_tests { mod integration_tests {
use anyhow::Ok;
use rota_lib::{ use rota_lib::{
config::{ToxicPair, UserConfig}, config::{ToxicPair, UserConfig},
resident::Resident, resident::Resident,
@@ -12,7 +13,7 @@ mod integration_tests {
#[fixture] #[fixture]
fn minimal_config() -> UserConfig { fn minimal_config() -> UserConfig {
UserConfig::new(2).with_residents(vec![ UserConfig::default().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"),
@@ -22,7 +23,7 @@ mod integration_tests {
#[fixture] #[fixture]
fn maximal_config() -> UserConfig { fn maximal_config() -> UserConfig {
UserConfig::new(2) UserConfig::default()
.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),
@@ -46,7 +47,7 @@ mod integration_tests {
#[fixture] #[fixture]
fn manual_shifts_heavy_config() -> UserConfig { fn manual_shifts_heavy_config() -> UserConfig {
UserConfig::new(2).with_residents(vec![ UserConfig::default().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),
@@ -65,7 +66,7 @@ mod integration_tests {
#[fixture] #[fixture]
fn complex_config() -> UserConfig { fn complex_config() -> UserConfig {
UserConfig::new(2) UserConfig::default()
.with_holidays(vec![5, 12, 19, 26]) .with_holidays(vec![5, 12, 19, 26])
.with_residents(vec![ .with_residents(vec![
Resident::new(1, "R1") Resident::new(1, "R1")
@@ -93,43 +94,57 @@ mod integration_tests {
} }
#[rstest] #[rstest]
fn test_minimal_config(minimal_config: UserConfig) { fn test_minimal_config(minimal_config: UserConfig) -> anyhow::Result<()> {
let mut schedule = MonthlySchedule::new(); let mut schedule = MonthlySchedule::new();
let mut tracker = WorkloadTracker::default(); let mut tracker = WorkloadTracker::default();
let scheduler = Scheduler::new_with_config(minimal_config.clone()); let scheduler = Scheduler::new_with_config(minimal_config.clone());
assert!(scheduler.run(&mut schedule, &mut tracker)); assert!(scheduler.run(&mut schedule, &mut tracker)?);
validate_all_constraints(&schedule, &tracker, &minimal_config); validate_all_constraints(&schedule, &tracker, &minimal_config);
Ok(())
} }
#[rstest] #[rstest]
fn test_maximal_config(maximal_config: UserConfig) { fn test_maximal_config(maximal_config: UserConfig) -> anyhow::Result<()> {
let mut schedule = MonthlySchedule::new(); let mut schedule = MonthlySchedule::new();
let mut tracker = WorkloadTracker::default(); let mut tracker = WorkloadTracker::default();
let scheduler = Scheduler::new_with_config(maximal_config.clone()); let scheduler = Scheduler::new_with_config(maximal_config.clone());
assert!(scheduler.run(&mut schedule, &mut tracker)); assert!(scheduler.run(&mut schedule, &mut tracker)?);
validate_all_constraints(&schedule, &tracker, &maximal_config); validate_all_constraints(&schedule, &tracker, &maximal_config);
Ok(())
} }
#[rstest] #[rstest]
fn test_manual_shifts_heavy_config(manual_shifts_heavy_config: UserConfig) { fn test_manual_shifts_heavy_config(
manual_shifts_heavy_config: UserConfig,
) -> anyhow::Result<()> {
let mut schedule = MonthlySchedule::new(); let mut schedule = MonthlySchedule::new();
let mut tracker = WorkloadTracker::default(); let mut tracker = WorkloadTracker::default();
let scheduler = Scheduler::new_with_config(manual_shifts_heavy_config.clone()); let scheduler = Scheduler::new_with_config(manual_shifts_heavy_config.clone());
assert!(scheduler.run(&mut schedule, &mut tracker)); assert!(scheduler.run(&mut schedule, &mut tracker)?);
validate_all_constraints(&schedule, &tracker, &manual_shifts_heavy_config); validate_all_constraints(&schedule, &tracker, &manual_shifts_heavy_config);
Ok(())
} }
#[rstest] #[rstest]
fn test_complex_config(complex_config: UserConfig) { fn test_complex_config(complex_config: UserConfig) -> anyhow::Result<()> {
let mut schedule = MonthlySchedule::new(); let mut schedule = MonthlySchedule::new();
let mut tracker = WorkloadTracker::default(); let mut tracker = WorkloadTracker::default();
let scheduler = Scheduler::new_with_config(complex_config.clone()); let scheduler = Scheduler::new_with_config(complex_config.clone());
assert!(scheduler.run(&mut schedule, &mut tracker)); assert!(scheduler.run(&mut schedule, &mut tracker)?);
validate_all_constraints(&schedule, &tracker, &complex_config); validate_all_constraints(&schedule, &tracker, &complex_config);
Ok(())
} }
fn validate_all_constraints( fn validate_all_constraints(