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    #[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}