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}