1#![cfg_attr(docsrs, feature(doc_cfg))]
34#![warn(missing_docs)]
35#![warn(rust_2018_idioms)]
36
37use std::future::Future;
38use std::time::{Duration, Instant};
39
40use dev_report::{CheckResult, Severity};
41
42pub async fn run_with_timeout<F, T>(
51 name: impl Into<String>,
52 timeout: Duration,
53 fut: F,
54) -> CheckResult
55where
56 F: Future<Output = T>,
57{
58 let name = name.into();
59 let started = Instant::now();
60 match tokio::time::timeout(timeout, fut).await {
61 Ok(_value) => {
62 let elapsed = started.elapsed();
63 CheckResult::pass(format!("async::{name}"))
64 .with_duration_ms(elapsed.as_millis() as u64)
65 }
66 Err(_elapsed) => CheckResult::fail(format!("async::{name}"), Severity::Error)
67 .with_detail(format!("future did not complete within {timeout:?}")),
68 }
69}
70
71pub async fn join_all_with_timeout<T>(
76 name: impl Into<String>,
77 timeout: Duration,
78 handles: Vec<tokio::task::JoinHandle<T>>,
79) -> Vec<CheckResult> {
80 let name = name.into();
81 let mut results = Vec::with_capacity(handles.len());
82 for (i, h) in handles.into_iter().enumerate() {
83 let task_name = format!("async::{name}::task{i}");
84 let started = Instant::now();
85 match tokio::time::timeout(timeout, h).await {
86 Ok(Ok(_)) => {
87 let elapsed = started.elapsed();
88 results.push(
89 CheckResult::pass(task_name).with_duration_ms(elapsed.as_millis() as u64),
90 );
91 }
92 Ok(Err(join_err)) => {
93 results.push(
94 CheckResult::fail(task_name, Severity::Critical)
95 .with_detail(format!("task panicked or was cancelled: {join_err}")),
96 );
97 }
98 Err(_) => {
99 results.push(
100 CheckResult::fail(task_name, Severity::Error)
101 .with_detail(format!("task did not complete within {timeout:?}")),
102 );
103 }
104 }
105 }
106 results
107}
108
109pub trait AsyncCheck {
111 type Output;
113 type Fut: Future<Output = Self::Output>;
115 fn run(self) -> Self::Fut;
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[tokio::test]
124 async fn timeout_pass_fast_future() {
125 let check = run_with_timeout("fast", Duration::from_millis(500), async {}).await;
126 assert!(matches!(check.verdict, dev_report::Verdict::Pass));
127 }
128
129 #[tokio::test]
130 async fn timeout_fail_slow_future() {
131 let check = run_with_timeout("slow", Duration::from_millis(10), async {
132 tokio::time::sleep(Duration::from_millis(200)).await;
133 })
134 .await;
135 assert!(matches!(check.verdict, dev_report::Verdict::Fail));
136 }
137
138 #[tokio::test]
139 async fn join_all_basic() {
140 let h1 = tokio::spawn(async { 1 });
141 let h2 = tokio::spawn(async { 2 });
142 let results = join_all_with_timeout("g", Duration::from_secs(1), vec![h1, h2]).await;
143 assert_eq!(results.len(), 2);
144 assert!(results
145 .iter()
146 .all(|r| matches!(r.verdict, dev_report::Verdict::Pass)));
147 }
148}