diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6cbac3b..5ff1737 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -497,7 +497,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2345,7 +2345,7 @@ dependencies = [ "once_cell", "png", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows-sys 0.60.2", ] @@ -3310,7 +3310,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3460,6 +3460,7 @@ dependencies = [ "tauri-build", "tauri-plugin-log", "tauri-plugin-opener", + "thiserror 2.0.18", ] [[package]] @@ -4144,7 +4145,7 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tray-icon", "url", @@ -4196,7 +4197,7 @@ dependencies = [ "sha2", "syn 2.0.111", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "url", "uuid", @@ -4252,7 +4253,7 @@ dependencies = [ "swift-rs", "tauri", "tauri-plugin", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -4272,7 +4273,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "windows", "zbus", @@ -4296,7 +4297,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "webkit2gtk", "webview2-com", @@ -4360,7 +4361,7 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.17", + "thiserror 2.0.18", "toml 0.9.8", "url", "urlpattern", @@ -4414,11 +4415,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -4434,9 +4435,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -4739,7 +4740,7 @@ dependencies = [ "once_cell", "png", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows-sys 0.60.2", ] @@ -5113,7 +5114,7 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", "windows", "windows-core 0.61.2", ] @@ -5623,7 +5624,7 @@ dependencies = [ "sha2", "soup3", "tao-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "webkit2gtk", "webkit2gtk-sys", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fc95a39..60a6db1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -31,6 +31,7 @@ rand = "0.9.2" docx-rs = "0.4.18" anyhow = "1.0.100" rayon = "1.11" +thiserror = "2.0.18" [dev-dependencies] criterion = { version = "0.8.1", features = ["html_reports"] } diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs new file mode 100644 index 0000000..782e2b9 --- /dev/null +++ b/src-tauri/src/errors.rs @@ -0,0 +1,59 @@ +use std::io; + +use serde::Serialize; +use thiserror::Error; + +#[derive(Error, Debug, Serialize)] +#[serde(tag = "kind", content = "details")] +pub enum SearchError { + #[error("another thread found a solution")] + SolutionFound, + #[error("time limit exceeded")] + Timeout, + #[error("schedule is already manually filled")] + ScheduleFull, + #[error("user configuration is invalid")] + Config(String), + #[error("no solution found")] + NoSolutionFound, +} + +impl From for SearchError { + fn from(err: anyhow::Error) -> Self { + SearchError::Config(err.to_string()) + } +} + +#[derive(Error, Debug)] +pub enum ExportError { + #[error("path not found: {0}")] + InvalidPath(#[from] io::Error), + #[error("docx packaging error: {0}")] + Packaging(String), + #[error("failed to open doc: {0}")] + OpenFailed(String), +} + +impl Serialize for ExportError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + + let mut s = serializer.serialize_struct("ExportError", 2)?; + + s.serialize_field( + "kind", + match self { + ExportError::InvalidPath(_) => "InvalidPath", + ExportError::Packaging(_) => "Packaging", + ExportError::OpenFailed(_) => "OpenFailed", + }, + )?; + + s.serialize_field("details", &self.to_string())?; + + s.end() + } +} diff --git a/src-tauri/src/export.rs b/src-tauri/src/export.rs index 6ab04c1..d83cf07 100644 --- a/src-tauri/src/export.rs +++ b/src-tauri/src/export.rs @@ -1,10 +1,10 @@ use std::{fs::File, io::Write}; -use anyhow::Context; use docx_rs::{Docx, Paragraph, Run, RunFonts, Table, TableCell, TableRow}; use crate::{ config::UserConfig, + errors::ExportError, schedule::MonthlySchedule, slot::{month_to_greek, weekday_to_greek, Day, ShiftPosition, Slot}, workload::WorkloadTracker, @@ -22,7 +22,7 @@ pub trait Export { file_type: FileType, config: &UserConfig, tracker: &WorkloadTracker, - ) -> anyhow::Result<()>; + ) -> Result<(), ExportError>; } impl Export for MonthlySchedule { @@ -31,7 +31,7 @@ impl Export for MonthlySchedule { file_type: FileType, config: &UserConfig, tracker: &WorkloadTracker, - ) -> anyhow::Result<()> { + ) -> Result<(), ExportError> { match file_type { FileType::Txt => self.export_as_txt(config, tracker)?, FileType::Docx => self.export_as_docx(config)?, @@ -46,7 +46,7 @@ impl MonthlySchedule { &self, config: &UserConfig, tracker: &WorkloadTracker, - ) -> anyhow::Result<()> { + ) -> Result<(), ExportError> { let file = File::create("rota.txt")?; let mut writer = std::io::BufWriter::new(file); @@ -157,15 +157,17 @@ impl MonthlySchedule { doc } - pub fn export_as_docx(&self, config: &UserConfig) -> anyhow::Result<()> { + pub fn export_as_docx(&self, config: &UserConfig) -> Result<(), ExportError> { let path = std::path::Path::new("rota.docx"); let file = std::fs::File::create(path)?; let doc = self.generate_docx(config); - doc.build().pack(file)?; + doc.build() + .pack(file) + .map_err(|e| ExportError::Packaging(e.to_string()))?; tauri_plugin_opener::open_path(path, None::<&str>) - .context("Created file but failed to open it")?; + .map_err(|e| ExportError::OpenFailed(e.to_string()))?; Ok(()) } @@ -173,7 +175,6 @@ impl MonthlySchedule { #[cfg(test)] mod tests { - use anyhow::Ok; use rstest::{fixture, rstest}; use crate::{ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7e157aa..1faa4fb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,9 +1,10 @@ use std::sync::Mutex; -use log::{error, info}; +use log::info; use crate::{ config::{UserConfig, UserConfigDTO}, + errors::{ExportError, SearchError}, export::{Export, FileType}, schedule::MonthlySchedule, scheduler::Scheduler, @@ -11,6 +12,7 @@ use crate::{ }; pub mod config; +pub mod errors; pub mod export; pub mod resident; pub mod schedule; @@ -32,16 +34,17 @@ struct AppState { fn generate( config: UserConfigDTO, state: tauri::State<'_, AppState>, -) -> Result { +) -> Result { let mut schedule = MonthlySchedule::new(); let mut tracker = WorkloadTracker::default(); - let config = UserConfig::try_from(config).map_err(|e| e.to_string())?; + let config = UserConfig::try_from(config)?; let scheduler = Scheduler::new_with_config(config.clone()); - scheduler - .run(&mut schedule, &mut tracker) - .inspect_err(|e| error!("{e}")) - .map_err(|e| e.to_string())?; + let solved = scheduler.run(&mut schedule, &mut tracker)?; + + if !solved { + return Err(SearchError::NoSolutionFound); + } info!( "Scheduler finished successfully in {}ms", @@ -60,19 +63,13 @@ fn generate( } #[tauri::command] -fn export(state: tauri::State<'_, AppState>) -> Result<(), String> { +fn export(state: tauri::State<'_, AppState>) -> Result<(), ExportError> { let schedule = state.schedule.lock().unwrap(); let tracker = state.tracker.lock().unwrap(); let config = state.config.lock().unwrap(); - 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())?; + schedule.export(FileType::Docx, &config, &tracker)?; + schedule.export(FileType::Txt, &config, &tracker)?; let log_dir = std::env::current_dir().unwrap_or(std::path::PathBuf::from(".")); info!("Files exported at {}", log_dir.display()); diff --git a/src-tauri/src/scheduler.rs b/src-tauri/src/scheduler.rs index 12d0afe..4055a40 100644 --- a/src-tauri/src/scheduler.rs +++ b/src-tauri/src/scheduler.rs @@ -2,6 +2,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use crate::{ config::UserConfig, + errors::SearchError, resident::ResidentId, schedule::MonthlySchedule, slot::Slot, @@ -9,10 +10,12 @@ use crate::{ workload::{WorkloadBounds, WorkloadTracker}, }; -use anyhow::bail; -use log::warn; +use log::info; use rand::Rng; -use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use rayon::{ + current_thread_index, + iter::{IntoParallelRefIterator, ParallelIterator}, +}; pub struct Scheduler { pub config: UserConfig, @@ -43,7 +46,7 @@ impl Scheduler { &self, schedule: &mut MonthlySchedule, tracker: &mut WorkloadTracker, - ) -> anyhow::Result { + ) -> Result { schedule.prefill(&self.config); for (slot, res_id) in schedule.0.iter() { tracker.insert(*res_id, &self.config, *slot); @@ -55,7 +58,7 @@ impl Scheduler { let slot = (0..=self.config.total_slots) .find(|&slot_idx| !schedule.0.contains_key(&Slot::from(slot_idx))) .map(Slot::from) - .ok_or_else(|| anyhow::anyhow!("Schedule is already full"))?; + .ok_or(SearchError::ScheduleFull)?; let resident_ids = self.valid_residents(slot, schedule); let solved_in_thread = AtomicBool::new(false); @@ -78,7 +81,7 @@ impl Scheduler { Ok(true) => Some((local_schedule, local_tracker)), Ok(false) => None, Err(e) => { - warn!("Search error: {}", e); + info!("Thread Id: [{}] {}", current_thread_index().unwrap(), e); None } } @@ -102,13 +105,13 @@ impl Scheduler { tracker: &mut WorkloadTracker, slot: Slot, solved_in_thread: &AtomicBool, - ) -> anyhow::Result { + ) -> Result { if solved_in_thread.load(Ordering::Relaxed) { - bail!("Another thread found the solution") + return Err(SearchError::SolutionFound); } if self.timer.limit_exceeded() { - bail!("Time exceeded. Restrictions too tight"); + return Err(SearchError::Timeout); } if !slot.is_first() @@ -132,10 +135,10 @@ impl Scheduler { // sort candidates by current workload, add rng for tie breakers let mut valid_resident_ids = self.valid_residents(slot, schedule); valid_resident_ids.sort_unstable_by_key(|res_id| { - let type_count = tracker.get_type_count(res_id, slot.shift_type()); - let workload = tracker.current_workload(res_id); - let tie_breaker: f64 = rand::rng().random(); - (type_count, workload, (tie_breaker * 1000.0) as usize) + let type_count = tracker.get_type_count(res_id, slot.shift_type()); + let workload = tracker.current_workload(res_id); + let tie_breaker: f64 = rand::rng().random(); + (type_count, workload, (tie_breaker * 1000.0) as usize) }); for id in valid_resident_ids { diff --git a/src-tauri/tests/integration.rs b/src-tauri/tests/integration.rs index a40ccfb..afcf1d8 100644 --- a/src-tauri/tests/integration.rs +++ b/src-tauri/tests/integration.rs @@ -1,6 +1,5 @@ #[cfg(test)] mod integration_tests { - use anyhow::Ok; use rota_lib::{ config::{ToxicPair, UserConfig}, resident::Resident, diff --git a/src/routes/components/schedule/generate.svelte b/src/routes/components/schedule/generate.svelte index 38ab18a..dfd63ee 100644 --- a/src/routes/components/schedule/generate.svelte +++ b/src/routes/components/schedule/generate.svelte @@ -34,16 +34,22 @@ return day % 2 === 0 ? 1 : 2; } + interface AppError { + kind: string; + details: string; + } + async function generate() { let config = rota.toDTO(); console.log(config); try { let schedule = await invoke("generate", { config }); - console.log("replyFromGenerate:", schedule); + console.log(schedule); rota.solution = schedule; } catch (error) { - console.error("Error:", error); + const { kind, details } = error as AppError; + console.error(`[${kind}] - ${details}`); } } @@ -53,7 +59,8 @@ try { await invoke("export", { schedule }); } catch (error) { - console.error("Error:", error); + const { kind, details } = error as AppError; + console.error(`[${kind}] - ${details}`); } }