Add custom errors with thiserror, log thread id
This commit is contained in:
35
src-tauri/Cargo.lock
generated
35
src-tauri/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
59
src-tauri/src/errors.rs
Normal file
59
src-tauri/src/errors.rs
Normal file
@@ -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<anyhow::Error> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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::{
|
||||
|
||||
@@ -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<MonthlySchedule, String> {
|
||||
) -> Result<MonthlySchedule, SearchError> {
|
||||
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());
|
||||
|
||||
@@ -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<bool> {
|
||||
) -> Result<bool, SearchError> {
|
||||
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<bool> {
|
||||
) -> Result<bool, SearchError> {
|
||||
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 {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#[cfg(test)]
|
||||
mod integration_tests {
|
||||
use anyhow::Ok;
|
||||
use rota_lib::{
|
||||
config::{ToxicPair, UserConfig},
|
||||
resident::Resident,
|
||||
|
||||
@@ -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<MonthlyScheduleDTO>("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}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user