Skip to main content

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    pub fn failure_point(&self) -> TimerFailurePoint {
129        self.failure_point
130    }
131
132    /// Returns the number of valid future-deadline registrations attempted.
133    ///
134    /// Foreign and already reached deadlines do not increment this count.
135    ///
136    /// # Returns
137    ///
138    /// The current registration count. Concurrent observations are intended
139    /// for test diagnostics and use relaxed ordering.
140    #[must_use]
141    #[inline(always)]
142    pub fn registration_count(&self) -> usize {
143        self.registration_count.load(Ordering::Relaxed)
144    }
145}
146
147impl Timer for FaultInjectingTimer {
148    /// Returns the fixture's private manual monotonic clock.
149    ///
150    /// # Returns
151    ///
152    /// The clock defining valid deadlines for this Timer.
153    #[inline(always)]
154    fn clock(&self) -> &dyn MonotonicClock {
155        &self.clock
156    }
157
158    /// Registers a future deadline and injects the configured failure.
159    ///
160    /// # Parameters
161    ///
162    /// * `deadline` - Absolute deadline expected in this Timer's clock domain.
163    ///
164    /// # Returns
165    ///
166    /// An immediately ready successful future for a reached deadline, or a
167    /// failing future when completion failure is configured.
168    ///
169    /// # Errors
170    ///
171    /// Returns [`TimeError::ClockDomainMismatch`] for a foreign deadline.
172    /// Returns the error factory's value directly when registration failure is
173    /// configured.
174    ///
175    /// # Panics
176    ///
177    /// Propagates a panic raised by the configured error factory.
178    fn at(&self, deadline: MonotonicInstant) -> Result<TimerFuture, TimeError> {
179        let now = self.clock.now();
180        deadline.validate_domain(now.domain())?;
181        if deadline <= now {
182            return Ok(Box::pin(std::future::ready(Ok(()))));
183        }
184        self.registration_count.fetch_add(1, Ordering::Relaxed);
185        let error = (self.error_factory)();
186        match self.failure_point {
187            TimerFailurePoint::Registration => Err(error),
188            TimerFailurePoint::Completion => {
189                Ok(Box::pin(std::future::ready(Err(error))))
190            }
191        }
192    }
193}