#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
use std::future::Future;
use std::time::{Duration, Instant};
use dev_report::{CheckResult, Severity};
pub async fn run_with_timeout<F, T>(
name: impl Into<String>,
timeout: Duration,
fut: F,
) -> CheckResult
where
F: Future<Output = T>,
{
let name = name.into();
let started = Instant::now();
match tokio::time::timeout(timeout, fut).await {
Ok(_value) => {
let elapsed = started.elapsed();
CheckResult::pass(format!("async::{name}"))
.with_duration_ms(elapsed.as_millis() as u64)
}
Err(_elapsed) => CheckResult::fail(format!("async::{name}"), Severity::Error)
.with_detail(format!("future did not complete within {timeout:?}")),
}
}
pub async fn join_all_with_timeout<T>(
name: impl Into<String>,
timeout: Duration,
handles: Vec<tokio::task::JoinHandle<T>>,
) -> Vec<CheckResult> {
let name = name.into();
let mut results = Vec::with_capacity(handles.len());
for (i, h) in handles.into_iter().enumerate() {
let task_name = format!("async::{name}::task{i}");
let started = Instant::now();
match tokio::time::timeout(timeout, h).await {
Ok(Ok(_)) => {
let elapsed = started.elapsed();
results.push(
CheckResult::pass(task_name).with_duration_ms(elapsed.as_millis() as u64),
);
}
Ok(Err(join_err)) => {
results.push(
CheckResult::fail(task_name, Severity::Critical)
.with_detail(format!("task panicked or was cancelled: {join_err}")),
);
}
Err(_) => {
results.push(
CheckResult::fail(task_name, Severity::Error)
.with_detail(format!("task did not complete within {timeout:?}")),
);
}
}
}
results
}
pub trait AsyncCheck {
type Output;
type Fut: Future<Output = Self::Output>;
fn run(self) -> Self::Fut;
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn timeout_pass_fast_future() {
let check = run_with_timeout("fast", Duration::from_millis(500), async {}).await;
assert!(matches!(check.verdict, dev_report::Verdict::Pass));
}
#[tokio::test]
async fn timeout_fail_slow_future() {
let check = run_with_timeout("slow", Duration::from_millis(10), async {
tokio::time::sleep(Duration::from_millis(200)).await;
})
.await;
assert!(matches!(check.verdict, dev_report::Verdict::Fail));
}
#[tokio::test]
async fn join_all_basic() {
let h1 = tokio::spawn(async { 1 });
let h2 = tokio::spawn(async { 2 });
let results = join_all_with_timeout("g", Duration::from_secs(1), vec![h1, h2]).await;
assert_eq!(results.len(), 2);
assert!(results
.iter()
.all(|r| matches!(r.verdict, dev_report::Verdict::Pass)));
}
}