Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "tasklet"
version = "0.2.11"
version = "0.2.12"
authors = ["Stavros Grigoriou <unix121@protonmail.com>"]
edition = "2021"
rust-version = "1.90.0"
Expand All @@ -13,11 +13,11 @@ keywords = ["cron", "scheduling", "tasks", "tasklet", "async"]
[dependencies]
cron = "0.15.0"
chrono = "0.4.42"
log = "0.4.28"
log = "0.4.29"
tokio = { version = "1.48.0", features = ["full"] }
futures = "0.3.31"
thiserror = "2.0.17"

[dev-dependencies]
simple_logger = "5.0.0"
simple_logger = "5.1.0"
tokio-test = "0.4.4"
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ in order to run tasks asynchronously.
|-----------|---------|
| cron | 0.15.0 |
| chrono | 0.4.42 |
| log | 0.4.28 |
| log | 0.4.29 |
| tokio | 1.48.0 |
| futures | 0.3.31 |
| thiserror | 2.0.17 |
Expand All @@ -34,7 +34,7 @@ In your `Cargo.toml` add:

```
[dependencies]
tasklet = "0.2.11"
tasklet = "0.2.12"
```

## Example
Expand Down
17 changes: 17 additions & 0 deletions src/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,21 @@ mod test {
.build();
assert!(result.is_err());
}

#[test]
fn test_task_builder_invalid_schedule() {
// Test with valid schedule
let result = TaskBuilder::new(chrono::Utc).every("* * * * * * *").build();
assert!(result.is_ok());

// Test with invalid schedule
let result = TaskBuilder::new(chrono::Utc).every("invalid cron").build();
assert!(result.is_err());

// Test that the error is the correct type
match result {
Err(TaskError::InvalidCronExpression(_)) => {} // Expected
_ => panic!("Expected InvalidCronExpression error"),
}
}
}
27 changes: 27 additions & 0 deletions src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,31 @@ mod test {
let mut task_gen = TaskGenerator::new("* * * * * * *", Local, || None);
assert!(task_gen.run().is_none());
}

#[test]
fn test_task_generator_scheduling() {
use chrono::Duration;

// Create a generator that executes every second
let mut generator = TaskGenerator::new("* * * * * * *", Utc, || {
Some(Task::new("* * * * * * *", None, None, Utc))
});

// Initial execution time should be within the next second
let now = Utc::now();
let expected_window_end = now + Duration::seconds(1);

assert!(generator.next_exec >= now);
assert!(generator.next_exec <= expected_window_end);

// After running, the next execution should be scheduled
let _ = generator.run();

// The next execution should be after the current time
assert!(generator.next_exec > now);

// The next execution should be within a second of the previous one
let second_execution_window_end = generator.next_exec + Duration::seconds(1);
assert!(generator.next_exec <= second_execution_window_end);
}
}
17 changes: 17 additions & 0 deletions src/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -474,4 +474,21 @@ mod test {
// The number of tasks should be zero again.
assert_eq!(scheduler.handles.len(), 1);
}

#[test]
fn test_task_builder_invalid_schedule() {
// Test with valid schedule
let result = TaskBuilder::new(chrono::Utc).every("* * * * * * *").build();
assert!(result.is_ok());

// Test with invalid schedule
let result = TaskBuilder::new(chrono::Utc).every("invalid cron").build();
assert!(result.is_err());

// Test that the error is the correct type
match result {
Err(TaskError::InvalidCronExpression(_)) => {} // Expected
_ => panic!("Expected InvalidCronExpression error"),
}
}
}
218 changes: 218 additions & 0 deletions src/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -682,4 +682,222 @@ mod test {
TaskError::InvalidCronExpression(_)
));
}

#[test]
fn test_task_status_transitions() {
let mut task = Task::new(
"* * * * * * *",
Some("Status transition test"),
Some(1),
Local,
)
.unwrap();

// Initial status should be Init
assert_eq!(task.status, Status::Init);

// After init(), status should be Scheduled
task.init();
assert_eq!(task.status, Status::Scheduled);

// Add a step that succeeds
task.add_step_default(|| Ok(TaskStepStatusOk::Success));

// After run_task(), status should be Executed
assert!(task.run_task().is_ok());
assert_eq!(task.status, Status::Executed);

// After reschedule(), with repeats=1 and already executed once,
// status should be Finished
assert!(task.reschedule().is_ok());
assert_eq!(task.status, Status::Finished);
}

#[test]
fn test_task_step_display() {
// Test with description
let step_with_desc = TaskStep::new("Test step", || Ok(TaskStepStatusOk::Success));
assert_eq!(format!("{}", step_with_desc), "Test step");

// Test without description
let step_no_desc = TaskStep::default(|| Ok(TaskStepStatusOk::Success));
assert_eq!(format!("{}", step_no_desc), "-");

// Test with empty description
let step_empty_desc = TaskStep::new("", || Ok(TaskStepStatusOk::Success));
assert_eq!(format!("{}", step_empty_desc), "-");
}

#[tokio::test]
async fn test_task_command_execution() {
use chrono::Duration;
use tokio::sync::oneshot;

let mut task = Task::new("* * * * * * *", Some("Command test"), Some(1), Utc).unwrap();
task.set_id(1);

// Add a simple step to ensure the Run command works properly
task.add_step("Test step", || Ok(TaskStepStatusOk::Success));

// Test Init command first - this should initialize the task
let (tx_init, rx_init) = oneshot::channel();
task.execute_command(TaskCmd::Init { sender: tx_init });
let init_response = rx_init.await.unwrap();
assert_eq!(init_response.id, 1);
assert_eq!(init_response.status, Status::Scheduled);

// Set next_exec to a future time to prevent automatic execution
let future_time = Utc::now() + Duration::seconds(10);
task.next_exec = Some(future_time);

// Send Run command - this should not execute the task since next_exec is in the future
let (tx_run_no_exec, rx_run_no_exec) = oneshot::channel();
task.execute_command(TaskCmd::Run {
sender: tx_run_no_exec,
});
let no_exec_response = rx_run_no_exec.await.unwrap();
assert_eq!(no_exec_response.id, 1);
assert_eq!(
no_exec_response.status,
Status::Scheduled,
"Task should remain Scheduled when next_exec is in the future"
);

// Set next_exec to a past time to allow execution
let past_time = Utc::now() - Duration::seconds(10);
task.next_exec = Some(past_time);

// Send Run command - this should execute the task
let (tx_run, rx_run) = oneshot::channel();
task.execute_command(TaskCmd::Run { sender: tx_run });
let run_response = rx_run.await.unwrap();
assert_eq!(run_response.id, 1);
assert_eq!(run_response.status, Status::Executed);

// Test Reschedule command
let (tx_reschedule, rx_reschedule) = oneshot::channel();
task.execute_command(TaskCmd::Reschedule {
sender: tx_reschedule,
});
let reschedule_response = rx_reschedule.await.unwrap();
assert_eq!(reschedule_response.id, 1);
assert_eq!(reschedule_response.status, Status::Finished);
}

#[test]
fn test_task_multiple_steps_execution() {
use std::sync::{Arc, Mutex};

// Create counters to track step execution
let counter1 = Arc::new(Mutex::new(0));
let counter2 = Arc::new(Mutex::new(0));
let counter3 = Arc::new(Mutex::new(0));

// Create a task with multiple steps
let mut task = Task::new("* * * * * * *", Some("Multiple steps"), Some(1), Local).unwrap();

// Add steps that increment counters
let c1 = counter1.clone();
task.add_step("Step 1", move || {
*c1.lock().unwrap() += 1;
Ok(TaskStepStatusOk::Success)
});

let c2 = counter2.clone();
task.add_step("Step 2", move || {
*c2.lock().unwrap() += 1;
Ok(TaskStepStatusOk::Success)
});

let c3 = counter3.clone();
task.add_step("Step 3", move || {
*c3.lock().unwrap() += 1;
Ok(TaskStepStatusOk::Success)
});

// Initialize and run
task.init();
assert!(task.run_task().is_ok());

// Verify all steps executed
assert_eq!(*counter1.lock().unwrap(), 1);
assert_eq!(*counter2.lock().unwrap(), 1);
assert_eq!(*counter3.lock().unwrap(), 1);

// Verify final status
assert_eq!(task.status, Status::Executed);
}

#[test]
fn test_task_step_failure_scenarios() {
use std::sync::{Arc, Mutex};

// Create a counter to verify which steps executed
let execution_counter = Arc::new(Mutex::new(Vec::new()));

// Create a task with three steps, where the second step fails
let mut task = Task::new("* * * * * * *", Some("Failure test"), None, Local).unwrap();

// First step succeeds
let counter = execution_counter.clone();
task.add_step("Step 1", move || {
counter.lock().unwrap().push(1);
Ok(TaskStepStatusOk::Success)
});

// Second step fails
let counter = execution_counter.clone();
task.add_step("Step 2", move || {
counter.lock().unwrap().push(2);
Err(TaskStepStatusErr::Error)
});

// Third step should not execute due to previous failure
let counter = execution_counter.clone();
task.add_step("Step 3", move || {
counter.lock().unwrap().push(3);
Ok(TaskStepStatusOk::Success)
});

// Initialize and run
task.init();
assert!(task.run_task().is_ok());

// Verify only steps 1 and 2 executed
assert_eq!(*execution_counter.lock().unwrap(), vec![1, 2]);

// Verify task is in Failed state
assert_eq!(task.status, Status::Failed);
}

#[test]
fn test_force_removal() {
// Create a task with a step that forces removal
let mut task = Task::new("* * * * * * *", Some("Force removal"), None, Local).unwrap();
task.add_step("Failing step", || Err(TaskStepStatusErr::ErrorDelete));

// Initialize and run
task.init();
assert!(task.run_task().is_ok());

// Verify task is marked for force removal
assert_eq!(task.status, Status::ForceRemoved);

// Verify reschedule respects force removal status
assert!(task.reschedule().is_ok());
assert_eq!(task.status, Status::ForceRemoved);
}

#[test]
fn test_empty_task_execution() {
// Create a task with no steps
let mut task = Task::new("* * * * * * *", Some("Empty task"), None, Local).unwrap();

// Initialize and run
task.init();
assert!(task.run_task().is_ok());

// Verify task is marked as Executed even with no steps
assert_eq!(task.status, Status::Executed);
}
}