diff --git a/Cargo.toml b/Cargo.toml index 2949809..c520aea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tasklet" -version = "0.2.8" +version = "0.2.9" authors = ["Stavros Grigoriou "] edition = "2021" rust-version = "1.85.1" @@ -12,11 +12,11 @@ keywords = ["cron", "scheduling", "tasks", "tasklet", "async"] [dependencies] cron = "0.15.0" -chrono = "0.4.40" -time = "0.3.41" +chrono = "0.4.41" log = "0.4.27" -tokio = { version = "1.44.1", features = ["full"] } +tokio = { version = "1.45.1", features = ["full"] } futures = "0.3.31" +thiserror = "2.0.12" [dev-dependencies] simple_logger = "5.0.0" diff --git a/README.md b/README.md index 42b6b2c..2b989a1 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,14 @@ in order to run tasks asynchronously. ## Dependencies -| library | version | -|---------|---------| -| cron | 0.15.0 | -| chrono | 0.4.40 | -| time | 0.3.41 | -| log | 0.4.27 | -| tokio | 1.44.1 | -| futures | 0.3.31 | +| library | version | +|-----------|---------| +| cron | 0.15.0 | +| chrono | 0.4.41 | +| log | 0.4.27 | +| tokio | 1.45.1 | +| futures | 0.3.31 | +| thiserror | 2.0.12 | ## How to use this library @@ -34,7 +34,7 @@ In your `Cargo.toml` add: ``` [dependencies] -tasklet = "0.2.8" +tasklet = "0.2.9" ``` ## Example @@ -64,7 +64,7 @@ async fn main() { // Create a task with 2 steps and add it to the scheduler. // The second step fails every second execution. // Append the task to the scheduler. - scheduler.add_task( + let _ = scheduler.add_task( TaskBuilder::new(chrono::Local) .every("1 * * * * * *") .description("A simple task") diff --git a/examples/force_remove_example.rs b/examples/force_remove_example.rs index 0ffaf00..48fe5af 100644 --- a/examples/force_remove_example.rs +++ b/examples/force_remove_example.rs @@ -20,7 +20,7 @@ async fn main() { // Create a task with 2 steps and add it to the scheduler. // The second step fails every second execution. // Append the task to the scheduler. - scheduler.add_task( + let _ = scheduler.add_task( TaskBuilder::new(chrono::Local) .every("0,5,10,15,20,25,30,35,40,45,50,55 * * * * * *") .description("A simple task") diff --git a/examples/one_task_example.rs b/examples/one_task_example.rs index 284d8d8..8e1e9ea 100644 --- a/examples/one_task_example.rs +++ b/examples/one_task_example.rs @@ -20,7 +20,7 @@ async fn main() { // Create a task with 2 steps and add it to the scheduler. // The second step fails every second execution. // Append the task to the scheduler. - scheduler.add_task( + let _ = scheduler.add_task( TaskBuilder::new(chrono::Local) .every("1 * * * * * *") .description("A simple task") diff --git a/examples/read_me_example.rs b/examples/read_me_example.rs index f86263a..a0d903c 100644 --- a/examples/read_me_example.rs +++ b/examples/read_me_example.rs @@ -20,7 +20,7 @@ async fn main() { // Create a task with 2 steps and add it to the scheduler. // The second step fails every second execution. // Append the task to the scheduler. - scheduler.add_task( + let _ = scheduler.add_task( TaskBuilder::new(chrono::Local) .every("1 * * * * * *") .description("A simple task") diff --git a/examples/simple_example.rs b/examples/simple_example.rs index 3cea538..d4e6247 100644 --- a/examples/simple_example.rs +++ b/examples/simple_example.rs @@ -32,6 +32,7 @@ async fn main() { }) .build(), ) + .unwrap() .add_task( TaskBuilder::new(chrono::Utc) .every("1, 10 , 20 * * * * * *") @@ -41,7 +42,8 @@ async fn main() { Ok(Success) }) .build(), - ); + ) + .unwrap(); // Execute the tasks in the queue. scheduler.run().await; diff --git a/examples/task_builder_example.rs b/examples/task_builder_example.rs index 3501a7e..d36c10c 100644 --- a/examples/task_builder_example.rs +++ b/examples/task_builder_example.rs @@ -14,7 +14,7 @@ async fn main() { let mut scheduler = TaskScheduler::new(500, Utc); // Append a new task with two steps. - scheduler.add_task( + let _ = scheduler.add_task( TaskBuilder::new(Utc) .every("* * * * * *") .description("Some description") diff --git a/src/builders.rs b/src/builders.rs index 816b061..a32f73c 100644 --- a/src/builders.rs +++ b/src/builders.rs @@ -1,3 +1,4 @@ +use crate::errors::{TaskError, TaskResult}; use crate::task::{Task, TaskStep, TaskStepStatusErr, TaskStepStatusOk}; use chrono::TimeZone; use cron::Schedule; @@ -16,6 +17,8 @@ where /// The provided `Schedule`, if not given, /// it will be defaulted to once every hour. schedule: Option, + /// The original expression string, for error reporting + expression: String, /// Max number of repeats. repeats: Option, /// The Task/Scheduler timezone. @@ -43,6 +46,7 @@ where steps: Vec::new(), description: None, schedule: None, + expression: "* * * * * * *".to_string(), // Default expression repeats: None, timezone, } @@ -56,7 +60,7 @@ where /// /// ```rust /// # use tasklet::TaskBuilder; - /// let _task = TaskBuilder::new(chrono::Local).every("* * * * * * *").description("Description").build(); + /// let _task = TaskBuilder::new(chrono::Local).every("* * * * * * *").description("Description").build().unwrap(); /// ``` pub fn description(mut self, description: &str) -> TaskBuilder { self.description = Some(description.to_string()); @@ -73,10 +77,19 @@ where /// /// ```rust /// # use tasklet::{TaskBuilder, Task}; - /// let _task = TaskBuilder::new(chrono::Local).every("* * * * * * *").build(); + /// let _task = TaskBuilder::new(chrono::Local).every("* * * * * * *").build().unwrap(); /// ``` pub fn every(mut self, expression: &str) -> TaskBuilder { - self.schedule = Some(expression.parse().unwrap()); + self.expression = expression.to_string(); + match expression.parse() { + Ok(schedule) => { + self.schedule = Some(schedule); + } + Err(_) => { + // We'll validate at build time + self.schedule = None; + } + }; self } @@ -144,24 +157,38 @@ where /// /// ```rust /// # use tasklet::{TaskBuilder, Task}; - /// let mut _task = TaskBuilder::new(chrono::Utc).build(); + /// let mut _task = TaskBuilder::new(chrono::Utc).build().unwrap(); /// ``` - pub fn build(self) -> Task { + pub fn build(self) -> TaskResult> { + // Validate schedule if provided + let schedule = match self.schedule { + Some(s) => s, + None => { + // Try to parse the expression + self.expression.parse().map_err(|e| { + TaskError::InvalidCronExpression(format!( + "Invalid cron expression '{}': {}", + self.expression, e + )) + })? + } + }; + + // Create the task with default expression - we'll replace the schedule after let mut task = Task::new( - "* * * * * * *", - match self.description { - Some(ref x) => Some(&x[..]), - None => None, - }, + "* * * * * * *", // This is just a placeholder, we'll set the real schedule next + self.description.as_deref(), self.repeats, self.timezone, - ); - task.set_schedule( - self.schedule - .unwrap_or_else(|| "* * * * * * *".parse().unwrap()), - ); + )?; + + // Set the validated schedule + task.set_schedule(schedule); + + // Set the steps task.set_steps(self.steps); - task + + Ok(task) } } @@ -197,7 +224,7 @@ mod test { #[test] pub fn test_task_builder_init() { let builder = TaskBuilder::new(chrono::Utc); - assert_none!(builder.repeats, builder.schedule, builder.description); + assert_none!(builder.repeats); assert_eq!(builder.steps.len(), 0); assert_eq!(builder.timezone, chrono::Utc); } @@ -206,7 +233,7 @@ mod test { #[test] pub fn test_task_builder_with_description() { let builder = TaskBuilder::new(chrono::Utc).description("Some description"); - assert_none!(builder.repeats, builder.schedule); + assert_none!(builder.repeats); assert_eq!(builder.steps.len(), 0); assert_some!(builder.description); assert_eq!(builder.timezone, chrono::Utc); @@ -229,14 +256,12 @@ mod test { assert_eq!(builder.timezone, chrono::Utc); assert_eq!(builder.steps.len(), 0); assert_some!(builder.repeats); - assert_none!(builder.schedule, builder.description); } /// Test the normal functionality of the add_step() function of the `TaskBuilder`. #[test] pub fn test_task_builder_add_step() { let builder = TaskBuilder::new(chrono::Utc).add_step_default(|| Ok(Success)); - assert_none!(builder.schedule, builder.repeats, builder.description); assert_eq!(builder.timezone, chrono::Utc); assert_eq!(builder.steps.len(), 1); } @@ -249,7 +274,8 @@ mod test { .repeat(5) .description("Some description") .add_step("Step 1", || Ok(Success)) - .build(); + .build() + .unwrap(); assert_some!(task.repeats); assert_eq!(task.description, "Some description"); assert_eq!(task.timezone, chrono::Utc); @@ -262,9 +288,19 @@ mod test { let task = TaskBuilder::new(chrono::Utc) .repeat(5) .add_step("Step 1", || Ok(Success)) - .build(); + .build() + .unwrap(); assert_some!(task.repeats); assert_eq!(task.timezone, chrono::Utc); assert_eq!(task.steps.len(), 1); } + + /// Test building with an invalid cron expression + #[test] + pub fn test_task_builder_invalid_expression() { + let result = TaskBuilder::new(chrono::Utc) + .every("invalid expression") + .build(); + assert!(result.is_err()); + } } diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..15641ed --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,42 @@ +//! Error types for the tasklet library. + +use thiserror::Error; + +/// Errors that can occur when working with tasks. +#[derive(Error, Debug)] +pub enum TaskError { + /// The task is not initialized yet. + #[error("Task not initialized yet")] + NotInitialized, + + /// The task has already been executed and must be rescheduled. + #[error("Task already executed and must be rescheduled")] + AlreadyExecuted, + + /// The task has failed and must be rescheduled. + #[error("Task failed and must be rescheduled")] + Failed, + + /// The task has finished and must be removed. + #[error("Task has finished and must be removed")] + Finished, + + /// The task has been force removed. + #[error("Task has been force removed")] + ForceRemoved, + + /// The task's schedule could not be parsed. + #[error("Invalid cron expression: {0}")] + InvalidCronExpression(String), + + /// A required component is missing. + #[error("Missing required component: {0}")] + MissingComponent(String), + + /// A generic error occurred during task execution. + #[error("Task execution error: {0}")] + ExecutionError(String), +} + +/// Result type for task operations. +pub type TaskResult = Result; diff --git a/src/generator.rs b/src/generator.rs index c45a715..4e36dc7 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -1,5 +1,6 @@ extern crate chrono; extern crate cron; +use crate::errors::TaskResult; use crate::task::Task; use chrono::prelude::*; use chrono::DateTime; @@ -15,7 +16,7 @@ where T: TimeZone + Send + 'static, { /// The discovery function, used to retrieve new tasks. - discovery_function: Box Option>)>, + discovery_function: Box Option>>)>, /// The execution schedule. schedule: Schedule, /// The task generator's timezone. @@ -47,7 +48,7 @@ where /// ``` pub fn new(expression: &str, timezone: T, function: F) -> TaskGenerator where - F: (FnMut() -> Option>) + 'static, + F: (FnMut() -> Option>>) + 'static, { let schedule: Schedule = expression.parse().unwrap(); @@ -60,7 +61,7 @@ where } /// Run the discovery function and reschedule the generation function. - pub(crate) fn run(&mut self) -> Option> { + pub(crate) fn run(&mut self) -> Option>> { debug!("Executing discovery function"); self.next_exec = self.schedule.upcoming(self.timezone.clone()).next()?; match (self.discovery_function)() { @@ -92,7 +93,7 @@ mod test { let mut task_gen = TaskGenerator::new("* * * * * * *", Local, || { Some(Task::new("* * * * * * *", None, Some(1), Local)) }); - assert!(!task_gen.run().is_none()); + assert!(task_gen.run().is_some()); } /// Test the normal flow of a task generation instance. diff --git a/src/lib.rs b/src/lib.rs index 0f9ec17..3692155 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,17 @@ +//! An asynchronous task scheduling library written in Rust. +//! +//! `tasklet` allows you to create scheduled tasks with specific execution patterns and +//! run them asynchronously using Tokio. It supports cron-like scheduling expressions and +//! provides a builder pattern for easy task creation. + mod builders; +pub mod errors; mod generator; mod scheduler; pub mod task; pub use builders::TaskBuilder; +pub use errors::{TaskError, TaskResult}; pub use generator::TaskGenerator; pub use scheduler::TaskScheduler; pub use task::Task; diff --git a/src/scheduler.rs b/src/scheduler.rs index 7ca60ba..b877f31 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -1,3 +1,4 @@ +use crate::errors::{TaskError, TaskResult}; use crate::generator::TaskGenerator; use crate::task::{run_task, Status, Task, TaskCmd, TaskResponse}; use chrono::prelude::*; @@ -132,27 +133,35 @@ where /// // Create a new `TaskScheduler` and attach a task to it. /// let mut scheduler = TaskScheduler::default(chrono::Local); /// // Add a task that executes every second forever. - /// scheduler.add_task(Task::new("* * * * * * *", None, None, chrono::Local)); + /// scheduler.add_task(Task::new("* * * * * * *", None, None, chrono::Local)).unwrap(); /// # }); /// ``` - pub fn add_task(&mut self, mut task: Task) -> &mut TaskScheduler { - let (sender, receiver) = mpsc::channel(32); - - task.set_receiver(receiver); - task.set_id(self.next_id); - let handle = tokio::spawn(run_task(task)); - - // Push the handle - self.handles.push(TaskHandle { - id: self.next_id, - handle, - sender, - is_init: false, - }); - - // Increase the id of the next task. - self.next_id += 1; - self + pub fn add_task( + &mut self, + task: TaskResult>, + ) -> Result<&mut TaskScheduler, TaskError> { + match task { + Ok(mut task) => { + let (sender, receiver) = mpsc::channel(32); + + task.set_receiver(receiver); + task.set_id(self.next_id); + let handle = tokio::spawn(run_task(task)); + + // Push the handle + self.handles.push(TaskHandle { + id: self.next_id, + handle, + sender, + is_init: false, + }); + + // Increase the id of the next task. + self.next_id += 1; + Ok(self) + } + Err(e) => Err(e), + } } /// Execute all the tasks in the queue. @@ -279,7 +288,7 @@ where if tg.next_exec <= Utc::now().with_timezone(&self.timezone) { return match tg.run() { Some(t) => { - self.add_task(t); + let _ = self.add_task(t); true } None => false, @@ -344,7 +353,9 @@ mod test { // Add a couple of tasks. scheduler .add_task(Task::new("* * * * * * *", None, Some(2), Local)) - .add_task(Task::new("* * * * * * *", None, None, Local)); + .unwrap() + .add_task(Task::new("* * * * * * *", None, None, Local)) + .unwrap(); assert_eq!(scheduler.handles.len(), 2); // Initialize the tasks. scheduler.init_tasks().await; @@ -363,12 +374,14 @@ mod test { // Create a new scheduler instance. let mut scheduler = TaskScheduler::new(500, Local); // Create a task. - let mut task = Task::new("* * * * * * *", None, Some(1), Local); + let mut task = Task::new("* * * * * * *", None, Some(1), Local).unwrap(); task.add_step_default(|| Err(ErrorDelete)); // Add a couple of tasks. scheduler - .add_task(task) - .add_task(Task::new("* * * * * * *", None, None, Local)); + .add_task(Ok(task)) + .unwrap() + .add_task(Task::new("* * * * * * *", None, None, Local)) + .unwrap(); assert_eq!(scheduler.handles.len(), 2); // Initialize the tasks. scheduler.init_tasks().await; @@ -400,13 +413,13 @@ mod test { let mut scheduler = TaskScheduler::new(500, Local); // Create a task. - let mut task = Task::new("* * * * * * *", None, Some(1), Local); + let mut task = Task::new("* * * * * * *", None, Some(1), Local).unwrap(); task.add_step_default(|| Ok(Success)); // Return an error in the second step. task.add_step_default(|| Err(Error)); // Add a task. - scheduler.add_task(task); + scheduler.add_task(Ok(task)).unwrap(); assert_eq!(scheduler.handles.len(), 1); // Initialize the task. scheduler.init_tasks().await; diff --git a/src/task.rs b/src/task.rs index bae75cb..65d2883 100644 --- a/src/task.rs +++ b/src/task.rs @@ -1,15 +1,16 @@ extern crate chrono; extern crate cron; +use crate::errors::{TaskError, TaskResult}; use chrono::TimeZone; use chrono::{DateTime, Utc}; use cron::Schedule; use log::{debug, error, warn}; -use std::fmt; +use std::fmt::{self, Debug}; use tokio::sync::{mpsc, oneshot}; /// Possible success status values for a step's execution. -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq)] pub enum TaskStepStatusOk { /// The step was a success, move to the next one (or exit if last). Success, @@ -18,7 +19,7 @@ pub enum TaskStepStatusOk { } /// Possible error status values for a step's execution. -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq)] pub enum TaskStepStatusErr { /// The task step execution failed. Error, @@ -167,6 +168,21 @@ where unsafe impl Send for Task where T: TimeZone + Send + 'static {} +impl Debug for Task +where + T: TimeZone + Send + 'static, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Task") + .field("task_id", &self.task_id) + .field("description", &self.description) + .field("status", &self.status) + .field("repeats", &self.repeats) + .field("next_exec", &self.next_exec) + .finish() + } +} + impl Task where T: TimeZone + Send + 'static, @@ -197,10 +213,15 @@ where description: Option<&str>, repeats: Option, timezone: T, - ) -> Task { - Task { + ) -> TaskResult> { + // Parse the schedule with proper error handling + let schedule = expression.parse().map_err(|e| { + TaskError::InvalidCronExpression(format!("Invalid cron expression: {}", e)) + })?; + + Ok(Task { steps: Vec::new(), - schedule: expression.parse().unwrap(), + schedule, description: match description { Some(s) => s.to_string(), None => "-".to_string(), @@ -211,7 +232,7 @@ where status: Status::default(), next_exec: None, receiver: None, - } + }) } /// Set the receiver for the task. @@ -231,7 +252,7 @@ where /// # use chrono::Utc; /// # use tasklet::task::Task; /// - /// let mut t = Task::new("* * * * * *", None, None, Utc); + /// let mut t = Task::new("* * * * * *", None, None, Utc).unwrap(); /// t.set_id(0); /// ``` pub fn set_id(&mut self, id: usize) { @@ -323,12 +344,12 @@ where if self.next_exec.as_ref().unwrap() <= &Utc::now().with_timezone(&self.timezone.clone()) { - self.run_task(); + let _ = self.run_task(); // Ignore the result as we already update the status } let _ = sender.send(self.get_task_response()); } TaskCmd::Reschedule { sender } => { - self.reschedule(); + let _ = self.reschedule(); // Ignore the result as we already update the status let _ = sender.send(self.get_task_response()); } TaskCmd::Init { sender } => { @@ -341,13 +362,13 @@ where } /// Run the task and handle the output. - pub(crate) fn run_task(&mut self) { + pub(crate) fn run_task(&mut self) -> TaskResult<()> { match &self.status { - Status::Init => panic!("Task not initialized yet!"), - Status::Failed => panic!("Task must be rescheduled!"), - Status::Executed => panic!("Task already executed and must be rescheduled!"), - Status::Finished => panic!("Task has finished and must be removed!"), - Status::ForceRemoved => panic!("Task has been forced removed!"), + Status::Init => Err(TaskError::NotInitialized), + Status::Failed => Err(TaskError::Failed), + Status::Executed => Err(TaskError::AlreadyExecuted), + Status::Finished => Err(TaskError::Finished), + Status::ForceRemoved => Err(TaskError::ForceRemoved), Status::Scheduled => { debug!( "[Task {}] [{}] is been executed...", @@ -391,14 +412,16 @@ where // Reduce the total executions (if set). self.repeats = self.repeats.map(|r| r - 1); + + Ok(()) } } } /// Reschedule the current task instance (if needed). - pub(crate) fn reschedule(&mut self) { + pub(crate) fn reschedule(&mut self) -> TaskResult<()> { match &self.status { - Status::Init => panic!("Task not initialized yet!"), + Status::Init => Err(TaskError::NotInitialized), Status::Failed | Status::Executed => { self.next_exec = Some( self.schedule @@ -420,13 +443,17 @@ where } } None => Status::Scheduled, - } + }; + Ok(()) } - Status::Finished | Status::ForceRemoved => warn!( - "[Task {}] The task will be removed from the queue", - self.task_id - ), - Status::Scheduled => { /* Do nothing, keep silent */ } + Status::Finished | Status::ForceRemoved => { + warn!( + "[Task {}] The task will be removed from the queue", + self.task_id + ); + Ok(()) + } + Status::Scheduled => Ok(()), /* Do nothing, keep silent */ } } } @@ -444,7 +471,7 @@ where /// # use tasklet::task::Task; /// # use tasklet::task::run_task; /// # tokio_test::block_on( async { -/// let t = Task::new("* * * * * *", None, None, Utc); +/// let t = Task::new("* * * * * *", None, None, Utc).unwrap(); /// let h = tokio::spawn(run_task(t)); /// # h.abort(); /// # }) @@ -473,26 +500,26 @@ mod test { #[test] fn normal_task_flow_test() { - let mut task = Task::new("* * * * * *", Some("Test task"), Some(2), Local); + let mut task = Task::new("* * * * * *", Some("Test task"), Some(2), Local).unwrap(); task.add_step_default(|| Ok(Success)); assert_eq!(task.status, Status::Init); task.set_id(0); task.init(); assert_eq!(task.status, Status::Scheduled); - task.run_task(); + assert!(task.run_task().is_ok()); assert_eq!(task.status, Status::Executed); - task.reschedule(); + assert!(task.reschedule().is_ok()); assert_eq!(task.status, Status::Scheduled); - task.run_task(); + assert!(task.run_task().is_ok()); assert_eq!(task.status, Status::Executed); - task.reschedule(); + assert!(task.reschedule().is_ok()); assert_eq!(task.status, Status::Finished); } #[test] fn test_task_set_schedule() { let schedule: Schedule = "* * * * * * *".parse().unwrap(); - let mut task = Task::new("* * * * * * *", None, None, Local); + let mut task = Task::new("* * * * * * *", None, None, Local).unwrap(); task.set_schedule(schedule); task.add_step_default(|| Ok(Success)); assert_eq!(task.status, Status::Init); @@ -503,26 +530,26 @@ mod test { #[test] fn normal_task_error_flow_test() { - let mut task = Task::new("* * * * * *", Some("Test task"), Some(2), Local); + let mut task = Task::new("* * * * * *", Some("Test task"), Some(2), Local).unwrap(); task.add_step_default(|| Err(Error)); assert_eq!(task.status, Status::Init); task.set_id(0); task.init(); assert_eq!(task.status, Status::Scheduled); - task.run_task(); + assert!(task.run_task().is_ok()); assert_eq!(task.status, Status::Failed); - task.reschedule(); + assert!(task.reschedule().is_ok()); assert_eq!(task.status, Status::Scheduled); - task.run_task(); + assert!(task.run_task().is_ok()); assert_eq!(task.status, Status::Failed); - task.reschedule(); + assert!(task.reschedule().is_ok()); assert_eq!(task.status, Status::Finished); } /// Test the normal execution of a simple task, without fixed repeats. #[test] fn normal_task_no_fixed_repeats_test() { - let mut task = Task::new("* * * * * * *", Some("Test task"), None, Local); + let mut task = Task::new("* * * * * * *", Some("Test task"), None, Local).unwrap(); task.add_step_default(|| Ok(Success)); assert_eq!(task.status, Status::Init); task.set_id(0); @@ -530,82 +557,104 @@ mod test { assert_eq!(task.status, Status::Scheduled); // Run it for a few times. for _i in 1..10 { - task.run_task(); + assert!(task.run_task().is_ok()); assert_eq!(task.status, Status::Executed); - task.reschedule(); + assert!(task.reschedule().is_ok()); assert_eq!(task.status, Status::Scheduled); } } #[test] - #[should_panic(expected = "Task not initialized yet!")] - fn test_reschedule_init_panic() { - let mut task = Task::new("* * * * * * *", None, None, Local); + fn test_reschedule_not_initialized() { + let mut task = Task::new("* * * * * * *", None, None, Local).unwrap(); // This task is not initialized, so it should fail. - task.reschedule(); + assert!(task.reschedule().is_err()); + assert!(matches!( + task.reschedule().unwrap_err(), + TaskError::NotInitialized + )); } #[test] fn test_reschedule_finished_should_mark_as_finished() { - let mut task = Task::new("* * * * * * *", None, Some(1), Local); + let mut task = Task::new("* * * * * * *", None, Some(1), Local).unwrap(); // Execute the task. task.set_id(0); task.init(); - task.run_task(); - task.reschedule(); + assert!(task.run_task().is_ok()); + assert!(task.reschedule().is_ok()); assert_eq!(task.status, Status::Finished); } #[test] - #[should_panic = "Task not initialized yet!"] fn test_run_uninitialized_task() { - let mut task = Task::new("* * * * * * *", None, None, Local); - task.run_task(); + let mut task = Task::new("* * * * * * *", None, None, Local).unwrap(); + assert!(task.run_task().is_err()); + assert!(matches!( + task.run_task().unwrap_err(), + TaskError::NotInitialized + )); } #[test] - #[should_panic = "Task must be rescheduled!"] fn test_run_failed_task() { - let mut task = Task::new("* * * * * * *", None, None, Local); + let mut task = Task::new("* * * * * * *", None, None, Local).unwrap(); task.add_step_default(|| Err(Error)); task.set_id(0); task.init(); - task.run_task(); + assert!(task.run_task().is_ok()); + assert_eq!(task.status, Status::Failed); // Attempt to rerun it, it should fail. - task.run_task(); + assert!(task.run_task().is_err()); + assert!(matches!(task.run_task().unwrap_err(), TaskError::Failed)); } #[test] - #[should_panic = "Task already executed and must be rescheduled!"] fn test_run_executed_task() { - let mut task = Task::new("* * * * * * *", None, None, Local); + let mut task = Task::new("* * * * * * *", None, None, Local).unwrap(); task.add_step("Step 1", || Ok(Success)); task.set_id(0); task.init(); - task.run_task(); + assert!(task.run_task().is_ok()); + assert_eq!(task.status, Status::Executed); // Attempt to run it again, it should fail. - task.run_task(); + assert!(task.run_task().is_err()); + assert!(matches!( + task.run_task().unwrap_err(), + TaskError::AlreadyExecuted + )); } #[test] - #[should_panic = "Task has finished and must be removed!"] fn test_run_finished_task() { - let mut task = Task::new("* * * * * * *", None, Some(1), Local); + let mut task = Task::new("* * * * * * *", None, Some(1), Local).unwrap(); task.set_id(0); task.init(); - task.run_task(); - task.reschedule(); + assert!(task.run_task().is_ok()); + assert!(task.reschedule().is_ok()); + assert_eq!(task.status, Status::Finished); // At this point the task is Finished. It should not be allowed to run again. - task.run_task(); + assert!(task.run_task().is_err()); + assert!(matches!(task.run_task().unwrap_err(), TaskError::Finished)); } #[test] fn test_run_failed_delete() { - let mut task = Task::new("* * * * * * *", None, None, Local); + let mut task = Task::new("* * * * * * *", None, None, Local).unwrap(); task.add_step_default(|| Err(ErrorDelete)); task.set_id(0); task.init(); - task.run_task(); + assert!(task.run_task().is_ok()); assert_eq!(task.status, Status::ForceRemoved); } + + #[test] + fn test_invalid_cron_expression() { + let task = Task::new("invalid expression", None, None, Local); + assert!(task.is_err()); + assert!(matches!( + task.unwrap_err(), + TaskError::InvalidCronExpression(_) + )); + } }