1use std::sync::Arc;
7use std::sync::OnceLock;
8use std::time::Duration;
9
10use tokio_util::sync::CancellationToken;
11
12#[derive(Debug, Clone)]
13pub enum CancelReason {
14 TestTimeout { duration: Duration },
17 SuiteTimeout { duration: Duration },
19 FailFast { trigger_test: String },
21 Sigint,
23}
24
25#[derive(Debug, Clone)]
26pub struct CancelToken {
27 inner: CancellationToken,
28 reason: Arc<OnceLock<CancelReason>>,
29 parent_reason: Option<Arc<OnceLock<CancelReason>>>,
30}
31
32impl CancelToken {
33 pub fn new() -> Self {
34 Self {
35 inner: CancellationToken::new(),
36 reason: Arc::new(OnceLock::new()),
37 parent_reason: None,
38 }
39 }
40
41 pub fn child(&self) -> Self {
46 Self {
47 inner: self.inner.child_token(),
48 reason: Arc::new(OnceLock::new()),
49 parent_reason: Some(self.reason.clone()),
50 }
51 }
52
53 pub fn cancel_with(&self, reason: CancelReason) {
55 let _ = self.reason.set(reason);
56 self.inner.cancel();
57 }
58
59 #[cfg(test)]
60 pub fn cancel(&self) {
61 self.inner.cancel();
62 }
63
64 pub fn is_cancelled(&self) -> bool {
65 self.inner.is_cancelled()
66 }
67
68 pub fn reason(&self) -> Option<CancelReason> {
69 self.reason
70 .get()
71 .cloned()
72 .or_else(|| self.parent_reason.as_ref().and_then(|p| p.get().cloned()))
73 }
74
75 pub async fn cancelled(&self) {
76 self.inner.cancelled().await
77 }
78}
79
80impl Default for CancelToken {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn cancel_with_sets_reason_and_flag() {
92 let t = CancelToken::new();
93 assert!(!t.is_cancelled());
94 assert!(t.reason().is_none());
95 t.cancel_with(CancelReason::Sigint);
96 assert!(t.is_cancelled());
97 assert!(matches!(t.reason(), Some(CancelReason::Sigint)));
98 }
99
100 #[test]
101 fn cancel_with_first_writer_wins() {
102 let t = CancelToken::new();
103 t.cancel_with(CancelReason::Sigint);
104 t.cancel_with(CancelReason::FailFast {
105 trigger_test: "x".into(),
106 });
107 assert!(matches!(t.reason(), Some(CancelReason::Sigint)));
108 }
109
110 #[test]
111 fn clone_shares_local_slot() {
112 let a = CancelToken::new();
113 let b = a.clone();
114 a.cancel_with(CancelReason::Sigint);
115 assert!(b.is_cancelled());
116 assert!(matches!(b.reason(), Some(CancelReason::Sigint)));
117 }
118
119 #[test]
120 fn child_observes_parent_cancel_and_reason() {
121 let parent = CancelToken::new();
122 let child = parent.child();
123 assert!(!child.is_cancelled());
124 parent.cancel_with(CancelReason::SuiteTimeout {
125 duration: Duration::from_secs(1),
126 });
127 assert!(child.is_cancelled());
128 match child.reason() {
129 Some(CancelReason::SuiteTimeout { duration }) => {
130 assert_eq!(duration, Duration::from_secs(1));
131 }
132 other => panic!("unexpected reason: {other:?}"),
133 }
134 }
135
136 #[test]
137 fn child_local_reason_does_not_bubble_up() {
138 let parent = CancelToken::new();
139 let child = parent.child();
140 child.cancel_with(CancelReason::TestTimeout {
141 duration: Duration::from_millis(300),
142 });
143 assert!(child.is_cancelled());
144 assert!(matches!(
145 child.reason(),
146 Some(CancelReason::TestTimeout { .. })
147 ));
148 assert!(!parent.is_cancelled());
149 assert!(parent.reason().is_none());
150 }
151
152 #[test]
153 fn child_local_reason_preferred_over_parent_fallback() {
154 let parent = CancelToken::new();
155 let child = parent.child();
156 parent.cancel_with(CancelReason::FailFast {
157 trigger_test: "p".into(),
158 });
159 child.cancel_with(CancelReason::TestTimeout {
160 duration: Duration::from_millis(100),
161 });
162 assert!(matches!(
163 child.reason(),
164 Some(CancelReason::TestTimeout { .. })
165 ));
166 assert!(matches!(
167 parent.reason(),
168 Some(CancelReason::FailFast { .. })
169 ));
170 }
171
172 #[test]
173 fn cfg_test_cancel_flips_flag_without_reason() {
174 let t = CancelToken::new();
175 t.cancel();
176 assert!(t.is_cancelled());
177 assert!(t.reason().is_none());
178 }
179}