Skip to main content

rust_supervisor/test_support/
factory.rs

1//! Deterministic helpers for supervisor tests.
2//!
3//! The module provides small reusable fixtures for event collection, paused
4//! time, and deterministic jitter.
5
6use crate::event::payload::{SupervisorEvent, What, Where};
7use crate::event::time::{CorrelationId, EventSequence, EventSequenceSource, EventTime, When};
8use crate::id::types::{ChildId, ChildStartCount, Generation, SupervisorPath};
9use crate::runtime::lifecycle::{RuntimeControlPlane, RuntimeExitReport};
10use crate::runtime::watchdog::RuntimeWatchdog;
11use crate::{control::handle::SupervisorHandle, runtime::message::RuntimeLoopMessage};
12use serde::{Deserialize, Serialize};
13use tokio::sync::{broadcast, mpsc};
14use uuid::Uuid;
15
16/// Paused time source for deterministic tests.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub struct PausedTime {
19    /// Wall-clock time in nanoseconds since the Unix epoch.
20    pub unix_nanos: u128,
21    /// Monotonic time in nanoseconds.
22    pub monotonic_nanos: u128,
23    /// Supervisor uptime in milliseconds.
24    pub uptime_ms: u64,
25}
26
27impl PausedTime {
28    /// Creates a paused time source.
29    ///
30    /// # Arguments
31    ///
32    /// - `unix_nanos`: Wall-clock timestamp in nanoseconds.
33    /// - `monotonic_nanos`: Monotonic timestamp in nanoseconds.
34    /// - `uptime_ms`: Supervisor uptime in milliseconds.
35    ///
36    /// # Returns
37    ///
38    /// Returns a [`PausedTime`] value.
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// let time = rust_supervisor::test_support::factory::PausedTime::new(1, 2, 3);
44    /// assert_eq!(time.uptime_ms, 3);
45    /// ```
46    pub fn new(unix_nanos: u128, monotonic_nanos: u128, uptime_ms: u64) -> Self {
47        Self {
48            unix_nanos,
49            monotonic_nanos,
50            uptime_ms,
51        }
52    }
53
54    /// Creates deterministic event time.
55    ///
56    /// # Arguments
57    ///
58    /// - `generation`: Child generation for the event.
59    /// - `child_start_count`: Child child_start_count for the event.
60    ///
61    /// # Returns
62    ///
63    /// Returns an [`EventTime`] value.
64    pub fn event_time(
65        &self,
66        generation: Generation,
67        child_start_count: ChildStartCount,
68    ) -> EventTime {
69        EventTime::deterministic(
70            self.unix_nanos,
71            self.monotonic_nanos,
72            self.uptime_ms,
73            generation,
74            child_start_count,
75        )
76    }
77}
78
79/// Deterministic jitter helper for backoff tests.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81pub struct DeterministicJitter {
82    /// Percentage points applied to the base delay.
83    pub percent: i64,
84}
85
86impl DeterministicJitter {
87    /// Creates a deterministic jitter source.
88    ///
89    /// # Arguments
90    ///
91    /// - `percent`: Signed percentage applied to the base delay.
92    ///
93    /// # Returns
94    ///
95    /// Returns a [`DeterministicJitter`] value.
96    pub fn new(percent: i64) -> Self {
97        Self { percent }
98    }
99
100    /// Applies jitter to a millisecond delay.
101    ///
102    /// # Arguments
103    ///
104    /// - `base_ms`: Base delay in milliseconds.
105    ///
106    /// # Returns
107    ///
108    /// Returns the adjusted delay in milliseconds.
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// let jitter = rust_supervisor::test_support::factory::DeterministicJitter::new(10);
114    /// assert_eq!(jitter.apply_ms(100), 110);
115    /// ```
116    pub fn apply_ms(&self, base_ms: u64) -> u64 {
117        let base = i128::from(base_ms);
118        let delta = base.saturating_mul(i128::from(self.percent)) / 100;
119        base.saturating_add(delta).max(0) as u64
120    }
121}
122
123/// Collector that stores supervisor events in memory.
124#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
125pub struct EventCollector {
126    /// Events collected in receive order.
127    pub events: Vec<SupervisorEvent>,
128}
129
130impl EventCollector {
131    /// Creates an empty collector.
132    ///
133    /// # Arguments
134    ///
135    /// This function has no arguments.
136    ///
137    /// # Returns
138    ///
139    /// Returns a new [`EventCollector`].
140    pub fn new() -> Self {
141        Self::default()
142    }
143
144    /// Pushes one event into the collector.
145    ///
146    /// # Arguments
147    ///
148    /// - `event`: Event to store.
149    ///
150    /// # Returns
151    ///
152    /// This function does not return a value.
153    pub fn push(&mut self, event: SupervisorEvent) {
154        self.events.push(event);
155    }
156
157    /// Returns collected event names.
158    ///
159    /// # Arguments
160    ///
161    /// This function has no arguments.
162    ///
163    /// # Returns
164    ///
165    /// Returns event names in receive order.
166    pub fn event_names(&self) -> Vec<&'static str> {
167        self.events.iter().map(|event| event.what.name()).collect()
168    }
169}
170
171/// Fixture that builds deterministic lifecycle events.
172#[derive(Debug)]
173pub struct EventFixture {
174    /// Paused time used for every event.
175    pub paused_time: PausedTime,
176    /// Sequence source used by the fixture.
177    pub sequences: EventSequenceSource,
178    /// Correlation identifier used by the fixture.
179    pub correlation_id: CorrelationId,
180    /// Configuration version attached to events.
181    pub config_version: u64,
182}
183
184impl EventFixture {
185    /// Creates an event fixture.
186    ///
187    /// # Arguments
188    ///
189    /// - `paused_time`: Time source for deterministic events.
190    /// - `config_version`: Configuration version attached to events.
191    ///
192    /// # Returns
193    ///
194    /// Returns an [`EventFixture`].
195    pub fn new(paused_time: PausedTime, config_version: u64) -> Self {
196        Self {
197            paused_time,
198            sequences: EventSequenceSource::new(),
199            correlation_id: CorrelationId::from_uuid(Uuid::nil()),
200            config_version,
201        }
202    }
203
204    /// Builds a deterministic event for a child.
205    ///
206    /// # Arguments
207    ///
208    /// - `child_id`: Child identifier attached to the event.
209    /// - `child_name`: Child name attached to the event.
210    /// - `what`: Event payload.
211    ///
212    /// # Returns
213    ///
214    /// Returns a [`SupervisorEvent`].
215    pub fn child_event(
216        &self,
217        child_id: ChildId,
218        child_name: impl Into<String>,
219        what: What,
220    ) -> SupervisorEvent {
221        let path = SupervisorPath::root().join(child_id.to_string());
222        let location = Where::new(path.clone()).with_child(child_id, child_name);
223        SupervisorEvent::new(
224            When::new(
225                self.paused_time
226                    .event_time(Generation::initial(), ChildStartCount::first()),
227            ),
228            location,
229            what,
230            self.sequences.next(),
231            self.correlation_id,
232            self.config_version,
233        )
234    }
235
236    /// Builds a deterministic event for the root supervisor.
237    ///
238    /// # Arguments
239    ///
240    /// - `what`: Event payload.
241    ///
242    /// # Returns
243    ///
244    /// Returns a [`SupervisorEvent`].
245    pub fn supervisor_event(&self, what: What) -> SupervisorEvent {
246        SupervisorEvent::new(
247            When::new(
248                self.paused_time
249                    .event_time(Generation::initial(), ChildStartCount::first()),
250            ),
251            Where::new(SupervisorPath::root()),
252            what,
253            self.sequences.next(),
254            self.correlation_id,
255            self.config_version,
256        )
257    }
258
259    /// Builds an event sequence value.
260    ///
261    /// # Arguments
262    ///
263    /// - `value`: Sequence value.
264    ///
265    /// # Returns
266    ///
267    /// Returns an [`EventSequence`].
268    pub fn sequence(value: u64) -> EventSequence {
269        EventSequence::new(value)
270    }
271}
272
273/// Creates a handle whose control loop has failed through a watchdog.
274///
275/// # Arguments
276///
277/// This function has no arguments.
278///
279/// # Returns
280///
281/// Returns a [`SupervisorHandle`] whose health report is failed.
282pub async fn runtime_control_plane_failed_handle() -> SupervisorHandle {
283    let (command_sender, command_receiver) = mpsc::channel::<RuntimeLoopMessage>(1);
284    drop(command_receiver);
285    let (event_sender, _) = broadcast::channel(16);
286    let control_plane = RuntimeControlPlane::new();
287    control_plane.mark_alive();
288    let join_handle = tokio::spawn(async move {
289        panic!("runtime control loop panic fixture");
290        #[allow(unreachable_code)]
291        RuntimeExitReport::completed("unreachable", "unreachable")
292    });
293    RuntimeWatchdog::spawn(control_plane.clone(), join_handle, event_sender.clone());
294    let handle = SupervisorHandle::new(command_sender, event_sender, control_plane);
295    let _report = handle.join().await.expect("failed runtime joins");
296    handle
297}
298
299/// Creates a backoff policy with deterministic jitter for reproducible tests.
300///
301/// # Arguments
302///
303/// - `initial`: Initial backoff delay.
304/// - `max`: Maximum backoff delay cap.
305/// - `jitter_percent`: Jitter percentage (0-100).
306/// - `reset_after`: Duration after which restart counters reset.
307/// - `seed`: Fixed RNG seed for deterministic jitter output.
308///
309/// # Returns
310///
311/// Returns a [`BackoffPolicy`] configured with deterministic jitter mode.
312///
313/// # Examples
314///
315/// ```
316/// use std::time::Duration;
317/// use rust_supervisor::test_support::factory::deterministic_backoff_policy;
318///
319/// let policy = deterministic_backoff_policy(
320///     Duration::from_millis(10),
321///     Duration::from_millis(1000),
322///     50,
323///     Duration::from_secs(300),
324///     42,
325/// );
326/// // Same seed produces identical delays across test runs
327/// let delay1 = policy.delay_for_child_start_count(1);
328/// let delay2 = policy.delay_for_child_start_count(1);
329/// assert_eq!(delay1, delay2);
330/// ```
331pub fn deterministic_backoff_policy(
332    initial: std::time::Duration,
333    max: std::time::Duration,
334    jitter_percent: u8,
335    reset_after: std::time::Duration,
336    seed: u64,
337) -> crate::policy::backoff::BackoffPolicy {
338    crate::policy::backoff::BackoffPolicy::new(initial, max, jitter_percent, reset_after)
339        .with_deterministic_jitter(seed)
340}
341
342/// Creates a backoff policy with full jitter for thundering herd prevention tests.
343///
344/// # Arguments
345///
346/// - `initial`: Initial backoff delay.
347/// - `max`: Maximum backoff delay cap.
348/// - `seed`: Fixed RNG seed for deterministic full jitter output.
349///
350/// # Returns
351///
352/// Returns a [`BackoffPolicy`] configured with full jitter mode.
353pub fn full_jitter_backoff_policy(
354    initial: std::time::Duration,
355    max: std::time::Duration,
356    seed: u64,
357) -> crate::policy::backoff::BackoffPolicy {
358    let mut policy = crate::policy::backoff::BackoffPolicy::new(
359        initial,
360        max,
361        100,
362        std::time::Duration::from_secs(300),
363    );
364    policy.jitter_mode = crate::policy::backoff::JitterMode::FullJitter { seed };
365    policy
366}
367
368/// Creates a backoff policy with decorrelated jitter for correlation-breaking tests.
369///
370/// # Arguments
371///
372/// - `initial`: Initial backoff delay.
373/// - `max`: Maximum backoff delay cap.
374/// - `seed`: Fixed RNG seed for deterministic decorrelated jitter output.
375///
376/// # Returns
377///
378/// Returns a [`BackoffPolicy`] configured with decorrelated jitter mode.
379pub fn decorrelated_jitter_backoff_policy(
380    initial: std::time::Duration,
381    max: std::time::Duration,
382    seed: u64,
383) -> crate::policy::backoff::BackoffPolicy {
384    let mut policy = crate::policy::backoff::BackoffPolicy::new(
385        initial,
386        max,
387        100,
388        std::time::Duration::from_secs(300),
389    );
390    policy.jitter_mode = crate::policy::backoff::JitterMode::DecorrelatedJitter { seed };
391    policy
392}