kithara_platform/common/cancel/
scope.rs1use std::fmt;
2
3use super::token::CancelToken;
4
5pub struct CancelScope {
21 token: CancelToken,
22}
23
24impl CancelScope {
25 #[must_use]
29 pub fn new(parent: Option<CancelToken>) -> Self {
30 let token = parent.map_or_else(CancelToken::root, |parent| parent.child());
31 Self { token }
32 }
33
34 pub fn cancel(&self) {
36 self.token.cancel();
37 }
38
39 #[must_use]
40 pub fn is_cancelled(&self) -> bool {
41 self.token.is_cancelled()
42 }
43
44 #[must_use]
47 pub fn token(&self) -> CancelToken {
48 self.token.clone()
49 }
50}
51
52impl fmt::Debug for CancelScope {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 f.debug_struct("CancelScope")
55 .field("cancelled", &self.is_cancelled())
56 .finish_non_exhaustive()
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use std::time::Duration;
63
64 use kithara_test_utils::kithara;
65
66 use super::CancelScope;
67 use crate::common::cancel::CancelToken;
68
69 #[kithara::test(timeout(Duration::from_secs(5)))]
70 fn standalone_scope_owns_uncancelled_root() {
71 let scope = CancelScope::new(None);
72 assert!(!scope.is_cancelled());
73 assert!(!scope.token().is_cancelled());
74 }
75
76 #[kithara::test(timeout(Duration::from_secs(5)))]
77 fn standalone_cancel_cancels_own_subtree() {
78 let scope = CancelScope::new(None);
79 let token = scope.token();
80 scope.cancel();
81 assert!(scope.is_cancelled());
82 assert!(token.is_cancelled());
83 }
84
85 #[kithara::test(timeout(Duration::from_secs(5)))]
86 fn composed_scope_observes_parent_cancel() {
87 let parent = CancelToken::never();
88 let scope = CancelScope::new(Some(parent.clone()));
89 let token = scope.token();
90 assert!(!scope.is_cancelled());
91 parent.cancel();
92 assert!(scope.is_cancelled());
93 assert!(token.is_cancelled());
94 }
95
96 #[kithara::test(timeout(Duration::from_secs(5)))]
97 fn composed_scope_cancel_does_not_cancel_parent() {
98 let parent = CancelToken::never();
101 let scope = CancelScope::new(Some(parent.clone()));
102 scope.cancel();
103 assert!(scope.is_cancelled());
104 assert!(
105 !parent.is_cancelled(),
106 "scope.cancel() must not cancel the parent token it was handed"
107 );
108 }
109
110 #[kithara::test(timeout(Duration::from_secs(5)))]
111 fn drop_is_passive() {
112 let parent = CancelToken::never();
114 let live = {
115 let scope = CancelScope::new(Some(parent.clone()));
116 scope.token()
117 };
118 assert!(
119 !live.is_cancelled() && !parent.is_cancelled(),
120 "dropping a scope must not cancel anything"
121 );
122 }
123}