use cano::prelude::*;
use std::borrow::Cow;
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Step {
Wait,
Next,
Done,
}
struct InherentTimer;
#[task::timer(state = Step)]
impl InherentTimer {
async fn wait(&self, _res: &Resources) -> Result<TimerOutcome, CanoError> {
Ok(TimerOutcome::Duration(Duration::from_millis(30)))
}
async fn after_wait(&self, _res: &Resources) -> Result<TaskResult<Step>, CanoError> {
Ok(TaskResult::Single(Step::Done))
}
}
#[test]
fn inherent_timer_has_default_minimal_config() {
assert_eq!(
TimerTask::<Step>::config(&InherentTimer)
.retry_mode
.max_attempts(),
1,
"TimerTask default config must be minimal (no retries)"
);
}
#[test]
fn inherent_timer_has_default_name() {
assert!(
TimerTask::<Step>::name(&InherentTimer).contains("InherentTimer"),
"default name should contain the type name"
);
}
#[tokio::test(start_paused = true)]
async fn inherent_timer_duration_waits_then_transitions() {
let start = tokio::time::Instant::now();
let res = Resources::new();
let result = Task::run(&InherentTimer, &res).await.unwrap();
let elapsed = start.elapsed();
assert_eq!(result, TaskResult::Single(Step::Done));
assert!(
elapsed >= Duration::from_millis(30),
"timer should sleep at least the requested duration, virtual elapsed = {elapsed:?}"
);
}
struct PastInstantTimer;
#[task::timer(state = Step)]
impl PastInstantTimer {
async fn wait(&self, _res: &Resources) -> Result<TimerOutcome, CanoError> {
Ok(TimerOutcome::Until(
Instant::now() - Duration::from_secs(60),
))
}
async fn after_wait(&self, _res: &Resources) -> Result<TaskResult<Step>, CanoError> {
Ok(TaskResult::Single(Step::Done))
}
}
#[tokio::test]
async fn inherent_timer_until_past_instant_completes_immediately() {
let res = Resources::new();
let start = Instant::now();
let result = Task::run(&PastInstantTimer, &res).await.unwrap();
assert_eq!(result, TaskResult::Single(Step::Done));
assert!(
start.elapsed() < Duration::from_millis(500),
"a past Until instant must not sleep"
);
}
struct SplitTimer;
#[task::timer(state = Step)]
impl SplitTimer {
async fn wait(&self, _res: &Resources) -> Result<TimerOutcome, CanoError> {
Ok(TimerOutcome::Duration(Duration::ZERO))
}
async fn after_wait(&self, _res: &Resources) -> Result<TaskResult<Step>, CanoError> {
Ok(TaskResult::Split(vec![Step::Wait, Step::Next]))
}
}
#[tokio::test]
async fn inherent_timer_after_wait_transition() {
let res = Resources::new();
let result = Task::run(&SplitTimer, &res).await.unwrap();
assert_eq!(result, TaskResult::Split(vec![Step::Wait, Step::Next]));
}
struct InherentCustomTimer;
#[task::timer(state = Step)]
impl InherentCustomTimer {
fn config(&self) -> TaskConfig {
TaskConfig::minimal().with_attempt_timeout(Duration::from_secs(5))
}
fn name(&self) -> Cow<'static, str> {
Cow::Borrowed("my-inherent-timer")
}
async fn wait(&self, _res: &Resources) -> Result<TimerOutcome, CanoError> {
Ok(TimerOutcome::Duration(Duration::ZERO))
}
async fn after_wait(&self, _res: &Resources) -> Result<TaskResult<Step>, CanoError> {
Ok(TaskResult::Single(Step::Done))
}
}
#[test]
fn inherent_custom_config_override() {
let cfg = TimerTask::<Step>::config(&InherentCustomTimer);
assert_eq!(cfg.retry_mode.max_attempts(), 1);
assert_eq!(cfg.attempt_timeout, Some(Duration::from_secs(5)));
}
#[test]
fn inherent_custom_name_override() {
assert_eq!(
TimerTask::<Step>::name(&InherentCustomTimer),
"my-inherent-timer"
);
}
#[test]
fn inherent_companion_task_forwards_config() {
let cfg = Task::config(&InherentCustomTimer);
assert_eq!(cfg.attempt_timeout, Some(Duration::from_secs(5)));
}
#[test]
fn inherent_companion_task_forwards_name() {
assert_eq!(Task::name(&InherentCustomTimer), "my-inherent-timer");
}
struct TraitTimer;
#[task::timer]
impl TimerTask<Step> for TraitTimer {
async fn wait(&self, _res: &Resources) -> Result<TimerOutcome, CanoError> {
Ok(TimerOutcome::Duration(Duration::ZERO))
}
async fn after_wait(&self, _res: &Resources) -> Result<TaskResult<Step>, CanoError> {
Ok(TaskResult::Single(Step::Done))
}
}
#[tokio::test]
async fn trait_form_timer_works() {
let res = Resources::new();
let result = Task::run(&TraitTimer, &res).await.unwrap();
assert_eq!(result, TaskResult::Single(Step::Done));
}
#[tokio::test]
async fn inherent_timer_as_dyn_task() {
let timer: Arc<dyn Task<Step>> = Arc::new(SplitTimer);
let res = Resources::new();
let result = Task::run(timer.as_ref(), &res).await.unwrap();
assert_eq!(result, TaskResult::Split(vec![Step::Wait, Step::Next]));
}
#[tokio::test]
async fn inherent_timer_integrates_with_workflow() {
let workflow = Workflow::bare()
.register(Step::Wait, TraitTimer)
.add_exit_state(Step::Done);
let result = workflow
.orchestrate(Step::Wait, CancellationToken::disabled())
.await
.unwrap();
assert_eq!(result, Step::Done);
}
struct WaitErrorTimer;
#[task::timer(state = Step)]
impl WaitErrorTimer {
async fn wait(&self, _res: &Resources) -> Result<TimerOutcome, CanoError> {
Err(CanoError::task_execution("wait failed deliberately"))
}
async fn after_wait(&self, _res: &Resources) -> Result<TaskResult<Step>, CanoError> {
Ok(TaskResult::Single(Step::Done))
}
}
#[tokio::test]
async fn wait_error_propagates_from_task_run() {
let res = Resources::new();
let err = Task::run(&WaitErrorTimer, &res).await.unwrap_err();
assert!(matches!(err, CanoError::TaskExecution(_)));
}
struct AfterWaitErrorTimer;
#[task::timer(state = Step)]
impl AfterWaitErrorTimer {
async fn wait(&self, _res: &Resources) -> Result<TimerOutcome, CanoError> {
Ok(TimerOutcome::Duration(Duration::ZERO))
}
async fn after_wait(&self, _res: &Resources) -> Result<TaskResult<Step>, CanoError> {
Err(CanoError::task_execution("after_wait failed deliberately"))
}
}
#[tokio::test]
async fn after_wait_error_propagates_from_task_run() {
let res = Resources::new();
let err = Task::run(&AfterWaitErrorTimer, &res).await.unwrap_err();
assert!(matches!(err, CanoError::TaskExecution(_)));
}