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