qubit_clock/test_util/fault_injecting_timer.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Defines a Timer that deterministically injects configured failures.
9
10use super::TimerFailurePoint;
11use crate::{
12 ManualMonotonicClock,
13 MonotonicClock,
14 MonotonicInstant,
15 TimeError,
16 Timer,
17 TimerFuture,
18 TimerUnavailableError,
19};
20use std::{
21 io,
22 sync::atomic::{
23 AtomicUsize,
24 Ordering,
25 },
26};
27
28/// A deterministic Timer fixture that fails registration or completion.
29///
30/// The fixture owns a private manual clock domain. Foreign deadlines and
31/// already reached deadlines retain the normal [`Timer`] contract; only valid
32/// future deadlines reach the configured failure point.
33///
34/// # Examples
35///
36/// ```
37/// use qubit_clock::{
38/// Timer,
39/// test_util::{FaultInjectingTimer, TimerFailurePoint},
40/// };
41/// use std::time::Duration;
42///
43/// let timer = FaultInjectingTimer::backend_unavailable(
44/// TimerFailurePoint::Registration,
45/// "example",
46/// "backend offline",
47/// );
48/// assert!(timer.after(Duration::from_secs(1)).is_err());
49/// assert_eq!(1, timer.registration_count());
50/// ```
51pub struct FaultInjectingTimer {
52 /// Manual clock defining the fixture's private monotonic domain.
53 clock: ManualMonotonicClock,
54 /// Timer lifecycle point where the configured error is returned.
55 failure_point: TimerFailurePoint,
56 /// Thread-safe factory producing one fresh error per failed registration.
57 error_factory: Box<dyn Fn() -> TimeError + Send + Sync + 'static>,
58 /// Number of valid future-deadline registrations attempted by the fixture.
59 registration_count: AtomicUsize,
60}
61
62impl FaultInjectingTimer {
63 /// Creates a Timer that invokes `error_factory` at `failure_point`.
64 ///
65 /// # Parameters
66 ///
67 /// * `failure_point` - Registration or completion stage to fail.
68 /// * `error_factory` - Thread-safe factory returning one fresh error for
69 /// every failed future-deadline registration.
70 ///
71 /// # Returns
72 ///
73 /// A fault-injecting Timer with a new private monotonic clock domain.
74 ///
75 /// # Panics
76 ///
77 /// Panics if process-wide clock-domain identifiers are exhausted.
78 #[must_use]
79 pub fn new<F>(failure_point: TimerFailurePoint, error_factory: F) -> Self
80 where
81 F: Fn() -> TimeError + Send + Sync + 'static,
82 {
83 Self {
84 clock: ManualMonotonicClock::new(),
85 failure_point,
86 error_factory: Box::new(error_factory),
87 registration_count: AtomicUsize::new(0),
88 }
89 }
90
91 /// Creates a Timer reporting a custom backend-unavailable error.
92 ///
93 /// # Parameters
94 ///
95 /// * `failure_point` - Registration or completion stage to fail.
96 /// * `backend` - Stable static name identifying the unavailable backend.
97 /// * `message` - Error message copied into each fresh source error.
98 ///
99 /// # Returns
100 ///
101 /// A fault-injecting Timer producing
102 /// [`TimerUnavailableError::BackendUnavailable`].
103 ///
104 /// # Panics
105 ///
106 /// Panics if process-wide clock-domain identifiers are exhausted.
107 #[must_use]
108 pub fn backend_unavailable(
109 failure_point: TimerFailurePoint,
110 backend: &'static str,
111 message: &str,
112 ) -> Self {
113 let message = message.to_owned();
114 Self::new(failure_point, move || TimeError::TimerUnavailable {
115 source: TimerUnavailableError::BackendUnavailable {
116 backend,
117 source: Box::new(io::Error::other(message.clone())),
118 },
119 })
120 }
121
122 /// Returns the configured Timer lifecycle failure point.
123 ///
124 /// # Returns
125 ///
126 /// Registration or completion according to fixture construction.
127 #[must_use]
128 #[inline(always)]
129 pub fn failure_point(&self) -> TimerFailurePoint {
130 self.failure_point
131 }
132
133 /// Returns the number of valid future-deadline registrations attempted.
134 ///
135 /// Foreign and already reached deadlines do not increment this count.
136 ///
137 /// # Returns
138 ///
139 /// The current registration count. Concurrent observations are intended
140 /// for test diagnostics and use relaxed ordering.
141 #[must_use]
142 #[inline(always)]
143 pub fn registration_count(&self) -> usize {
144 self.registration_count.load(Ordering::Relaxed)
145 }
146}
147
148impl Timer for FaultInjectingTimer {
149 /// Returns the fixture's private manual monotonic clock.
150 ///
151 /// # Returns
152 ///
153 /// The clock defining valid deadlines for this Timer.
154 #[inline(always)]
155 fn clock(&self) -> &dyn MonotonicClock {
156 &self.clock
157 }
158
159 /// Registers a future deadline and injects the configured failure.
160 ///
161 /// # Parameters
162 ///
163 /// * `deadline` - Absolute deadline expected in this Timer's clock domain.
164 ///
165 /// # Returns
166 ///
167 /// An immediately ready successful future for a reached deadline, or a
168 /// failing future when completion failure is configured.
169 ///
170 /// # Errors
171 ///
172 /// Returns [`TimeError::ClockDomainMismatch`] for a foreign deadline.
173 /// Returns the error factory's value directly when registration failure is
174 /// configured.
175 ///
176 /// # Panics
177 ///
178 /// Propagates a panic raised by the configured error factory.
179 fn at(&self, deadline: MonotonicInstant) -> Result<TimerFuture, TimeError> {
180 let now = self.clock.now();
181 deadline.validate_domain(now.domain())?;
182 if deadline <= now {
183 return Ok(Box::pin(std::future::ready(Ok(()))));
184 }
185 self.registration_count.fetch_add(1, Ordering::Relaxed);
186 let error = (self.error_factory)();
187 match self.failure_point {
188 TimerFailurePoint::Registration => Err(error),
189 TimerFailurePoint::Completion => {
190 Ok(Box::pin(std::future::ready(Err(error))))
191 }
192 }
193 }
194}