xerv_core/testing/
context.rs

1//! Test context and builder for deterministic testing.
2//!
3//! Provides a configurable context for testing flows with mock providers.
4
5use super::providers::{
6    ClockProvider, EnvProvider, FsProvider, HttpProvider, MockClock, MockEnv, MockFs, MockHttp,
7    MockRng, MockSecrets, MockUuid, RealClock, RealEnv, RealFs, RealHttp, RealRng, RealUuid,
8    RngProvider, SecretsProvider, UuidProvider,
9};
10use super::recording::EventRecorder;
11use crate::arena::{ArenaConfig, ArenaReader, ArenaWriter};
12use crate::error::Result;
13use crate::types::{ArenaOffset, NodeId, RelPtr, TraceId};
14use crate::wal::{Wal, WalConfig};
15use std::sync::Arc;
16
17/// Test context with pluggable providers for deterministic testing.
18///
19/// This context provides access to all the same functionality as the regular
20/// `Context`, plus mock providers for external dependencies.
21///
22/// # Example
23///
24/// ```ignore
25/// use xerv_core::testing::{TestContextBuilder, MockClock, MockHttp};
26///
27/// let ctx = TestContextBuilder::new()
28///     .with_fixed_time("2024-01-15T10:30:00Z")
29///     .with_seed(42)
30///     .with_sequential_uuids()
31///     .with_recording()
32///     .build()
33///     .unwrap();
34///
35/// // Use providers
36/// let time = ctx.clock.system_time_millis();
37/// let uuid = ctx.uuid.new_v4();
38/// ```
39pub struct TestContext {
40    // Core context fields
41    trace_id: TraceId,
42    node_id: NodeId,
43    reader: ArenaReader,
44    writer: ArenaWriter,
45    wal: Arc<Wal>,
46
47    // Providers
48    /// Clock provider for time operations.
49    pub clock: Arc<dyn ClockProvider>,
50    /// HTTP provider for network requests.
51    pub http: Arc<dyn HttpProvider>,
52    /// RNG provider for random number generation.
53    pub rng: Arc<dyn RngProvider>,
54    /// UUID provider for UUID generation.
55    pub uuid: Arc<dyn UuidProvider>,
56    /// Filesystem provider for file operations.
57    pub fs: Arc<dyn FsProvider>,
58    /// Environment provider for environment variables.
59    pub env: Arc<dyn EnvProvider>,
60    /// Secrets provider for secret management.
61    pub secrets: Arc<dyn SecretsProvider>,
62
63    // Recording
64    recorder: Option<Arc<EventRecorder>>,
65}
66
67impl TestContext {
68    /// Get the trace ID.
69    pub fn trace_id(&self) -> TraceId {
70        self.trace_id
71    }
72
73    /// Get the node ID.
74    pub fn node_id(&self) -> NodeId {
75        self.node_id
76    }
77
78    /// Read bytes from the arena.
79    pub fn read_bytes(&self, ptr: RelPtr<()>) -> Result<Vec<u8>> {
80        self.reader.read_bytes(ptr.offset(), ptr.size() as usize)
81    }
82
83    /// Write bytes to the arena.
84    pub fn write_bytes(&self, bytes: &[u8]) -> Result<RelPtr<()>> {
85        self.writer.write_bytes(bytes)
86    }
87
88    /// Read raw bytes from the arena.
89    pub fn read_raw(&self, offset: ArenaOffset, size: usize) -> Result<Vec<u8>> {
90        self.reader.read_bytes(offset, size)
91    }
92
93    /// Write raw bytes to the arena.
94    pub fn write_raw(&self, bytes: &[u8]) -> Result<RelPtr<()>> {
95        self.writer.write_bytes(bytes)
96    }
97
98    /// Log a message.
99    pub fn log(&self, message: impl AsRef<str>) {
100        tracing::info!(
101            trace_id = %self.trace_id,
102            node_id = %self.node_id,
103            "{}",
104            message.as_ref()
105        );
106    }
107
108    /// Log a warning.
109    pub fn warn(&self, message: impl AsRef<str>) {
110        tracing::warn!(
111            trace_id = %self.trace_id,
112            node_id = %self.node_id,
113            "{}",
114            message.as_ref()
115        );
116    }
117
118    /// Log an error.
119    pub fn error(&self, message: impl AsRef<str>) {
120        tracing::error!(
121            trace_id = %self.trace_id,
122            node_id = %self.node_id,
123            "{}",
124            message.as_ref()
125        );
126    }
127
128    /// Get the current write position.
129    pub fn write_position(&self) -> ArenaOffset {
130        self.writer.write_position()
131    }
132
133    /// Get the WAL.
134    pub fn wal(&self) -> &Wal {
135        &self.wal
136    }
137
138    /// Get the event recorder (if recording is enabled).
139    pub fn recorder(&self) -> Option<&Arc<EventRecorder>> {
140        self.recorder.as_ref()
141    }
142
143    /// Record an event (if recording is enabled).
144    pub fn record(&self, event: super::recording::RecordedEvent) {
145        if let Some(recorder) = &self.recorder {
146            recorder.record(event);
147        }
148    }
149
150    // Provider-based operations with recording
151
152    /// Get current time in nanoseconds.
153    pub fn now(&self) -> u64 {
154        let nanos = self.clock.now();
155        self.record(super::recording::RecordedEvent::ClockNow { nanos });
156        nanos
157    }
158
159    /// Get current system time in milliseconds.
160    pub fn system_time_millis(&self) -> u64 {
161        let millis = self.clock.system_time_millis();
162        self.record(super::recording::RecordedEvent::SystemTime { millis });
163        millis
164    }
165
166    /// Generate a new UUID.
167    pub fn new_uuid(&self) -> uuid::Uuid {
168        let uuid = self.uuid.new_v4();
169        self.record(super::recording::RecordedEvent::UuidGenerated {
170            uuid: uuid.to_string(),
171        });
172        uuid
173    }
174
175    /// Generate a random u64.
176    pub fn random_u64(&self) -> u64 {
177        let value = self.rng.next_u64();
178        self.record(super::recording::RecordedEvent::RandomU64 { value });
179        value
180    }
181
182    /// Generate a random f64 in [0, 1).
183    pub fn random_f64(&self) -> f64 {
184        let value = self.rng.next_f64();
185        self.record(super::recording::RecordedEvent::RandomF64 { value });
186        value
187    }
188
189    /// Get an environment variable.
190    pub fn env_var(&self, key: &str) -> Option<String> {
191        let value = self.env.var(key);
192        self.record(super::recording::RecordedEvent::EnvRead {
193            key: key.to_string(),
194            value: value.clone(),
195        });
196        value
197    }
198
199    /// Get a secret.
200    pub fn secret(&self, key: &str) -> Option<String> {
201        let value = self.secrets.get(key);
202        self.record(super::recording::RecordedEvent::SecretRead {
203            key: key.to_string(),
204            found: value.is_some(),
205        });
206        value
207    }
208}
209
210/// Builder for creating test contexts with configurable providers.
211///
212/// # Example
213///
214/// ```ignore
215/// use xerv_core::testing::TestContextBuilder;
216///
217/// let ctx = TestContextBuilder::new()
218///     .with_fixed_time("2024-01-15T10:30:00Z")
219///     .with_mock_http(
220///         MockHttp::new()
221///             .on_get("https://api.example.com/status")
222///             .respond_json(200, serde_json::json!({"ok": true}))
223///     )
224///     .with_seed(42)
225///     .with_sequential_uuids()
226///     .with_env_vars(&[("DEBUG", "true")])
227///     .with_secrets(&[("API_KEY", "test-key")])
228///     .with_recording()
229///     .build()
230///     .unwrap();
231/// ```
232pub struct TestContextBuilder {
233    trace_id: Option<TraceId>,
234    node_id: Option<NodeId>,
235    arena_config: ArenaConfig,
236    wal_config: WalConfig,
237
238    clock: Option<Arc<dyn ClockProvider>>,
239    http: Option<Arc<dyn HttpProvider>>,
240    rng: Option<Arc<dyn RngProvider>>,
241    uuid: Option<Arc<dyn UuidProvider>>,
242    fs: Option<Arc<dyn FsProvider>>,
243    env: Option<Arc<dyn EnvProvider>>,
244    secrets: Option<Arc<dyn SecretsProvider>>,
245
246    recording: bool,
247}
248
249impl TestContextBuilder {
250    /// Create a new test context builder with default settings.
251    pub fn new() -> Self {
252        Self {
253            trace_id: None,
254            node_id: None,
255            arena_config: ArenaConfig::in_memory(),
256            wal_config: WalConfig::in_memory(),
257
258            clock: None,
259            http: None,
260            rng: None,
261            uuid: None,
262            fs: None,
263            env: None,
264            secrets: None,
265
266            recording: false,
267        }
268    }
269
270    /// Set the trace ID.
271    pub fn with_trace_id(mut self, trace_id: TraceId) -> Self {
272        self.trace_id = Some(trace_id);
273        self
274    }
275
276    /// Set the node ID.
277    pub fn with_node_id(mut self, node_id: NodeId) -> Self {
278        self.node_id = Some(node_id);
279        self
280    }
281
282    /// Set the arena configuration.
283    pub fn with_arena_config(mut self, config: ArenaConfig) -> Self {
284        self.arena_config = config;
285        self
286    }
287
288    /// Set the WAL configuration.
289    pub fn with_wal_config(mut self, config: WalConfig) -> Self {
290        self.wal_config = config;
291        self
292    }
293
294    // Clock providers
295
296    /// Use a fixed time.
297    pub fn with_fixed_time(mut self, iso_time: &str) -> Self {
298        self.clock = Some(Arc::new(MockClock::fixed(iso_time)));
299        self
300    }
301
302    /// Use a mock clock starting at time zero.
303    pub fn with_mock_clock(mut self) -> Self {
304        self.clock = Some(Arc::new(MockClock::new()));
305        self
306    }
307
308    /// Use a custom clock provider.
309    pub fn with_clock(mut self, clock: Arc<dyn ClockProvider>) -> Self {
310        self.clock = Some(clock);
311        self
312    }
313
314    /// Use the real system clock.
315    pub fn with_real_clock(mut self) -> Self {
316        self.clock = Some(Arc::new(RealClock::new()));
317        self
318    }
319
320    // HTTP providers
321
322    /// Use a mock HTTP provider.
323    pub fn with_mock_http(mut self, mock: MockHttp) -> Self {
324        self.http = Some(Arc::new(mock));
325        self
326    }
327
328    /// Use a custom HTTP provider.
329    pub fn with_http(mut self, http: Arc<dyn HttpProvider>) -> Self {
330        self.http = Some(http);
331        self
332    }
333
334    /// Use the real HTTP provider.
335    pub fn with_real_http(mut self) -> Self {
336        self.http = Some(Arc::new(RealHttp::new()));
337        self
338    }
339
340    // RNG providers
341
342    /// Use a seeded RNG for deterministic behavior.
343    pub fn with_seed(mut self, seed: u64) -> Self {
344        self.rng = Some(Arc::new(MockRng::seeded(seed)));
345        self
346    }
347
348    /// Use a custom RNG provider.
349    pub fn with_rng(mut self, rng: Arc<dyn RngProvider>) -> Self {
350        self.rng = Some(rng);
351        self
352    }
353
354    /// Use the real RNG.
355    pub fn with_real_rng(mut self) -> Self {
356        self.rng = Some(Arc::new(RealRng::new()));
357        self
358    }
359
360    // UUID providers
361
362    /// Use sequential UUIDs (00000001, 00000002, ...).
363    pub fn with_sequential_uuids(mut self) -> Self {
364        self.uuid = Some(Arc::new(MockUuid::sequential()));
365        self
366    }
367
368    /// Use predetermined UUIDs.
369    pub fn with_predetermined_uuids(mut self, uuids: &[&str]) -> Self {
370        self.uuid = Some(Arc::new(MockUuid::from_strings(uuids)));
371        self
372    }
373
374    /// Use a custom UUID provider.
375    pub fn with_uuid(mut self, uuid: Arc<dyn UuidProvider>) -> Self {
376        self.uuid = Some(uuid);
377        self
378    }
379
380    /// Use the real UUID provider.
381    pub fn with_real_uuid(mut self) -> Self {
382        self.uuid = Some(Arc::new(RealUuid::new()));
383        self
384    }
385
386    // Filesystem providers
387
388    /// Use an in-memory filesystem.
389    pub fn with_memory_fs(mut self) -> Self {
390        self.fs = Some(Arc::new(MockFs::new()));
391        self
392    }
393
394    /// Use a mock filesystem with predefined files.
395    pub fn with_mock_fs(mut self, mock: MockFs) -> Self {
396        self.fs = Some(Arc::new(mock));
397        self
398    }
399
400    /// Use a custom filesystem provider.
401    pub fn with_fs(mut self, fs: Arc<dyn FsProvider>) -> Self {
402        self.fs = Some(fs);
403        self
404    }
405
406    /// Use the real filesystem.
407    pub fn with_real_fs(mut self) -> Self {
408        self.fs = Some(Arc::new(RealFs::new()));
409        self
410    }
411
412    // Environment providers
413
414    /// Use mock environment variables.
415    pub fn with_env_vars(mut self, vars: &[(&str, &str)]) -> Self {
416        self.env = Some(Arc::new(MockEnv::from_pairs(vars)));
417        self
418    }
419
420    /// Use a mock environment provider.
421    pub fn with_mock_env(mut self, mock: MockEnv) -> Self {
422        self.env = Some(Arc::new(mock));
423        self
424    }
425
426    /// Use a custom environment provider.
427    pub fn with_env(mut self, env: Arc<dyn EnvProvider>) -> Self {
428        self.env = Some(env);
429        self
430    }
431
432    /// Use the real environment.
433    pub fn with_real_env(mut self) -> Self {
434        self.env = Some(Arc::new(RealEnv::new()));
435        self
436    }
437
438    // Secrets providers
439
440    /// Use mock secrets.
441    pub fn with_secrets(mut self, secrets: &[(&str, &str)]) -> Self {
442        self.secrets = Some(Arc::new(MockSecrets::from_pairs(secrets)));
443        self
444    }
445
446    /// Use a mock secrets provider.
447    pub fn with_mock_secrets(mut self, mock: MockSecrets) -> Self {
448        self.secrets = Some(Arc::new(mock));
449        self
450    }
451
452    /// Use a custom secrets provider.
453    pub fn with_secrets_provider(mut self, secrets: Arc<dyn SecretsProvider>) -> Self {
454        self.secrets = Some(secrets);
455        self
456    }
457
458    // Recording
459
460    /// Enable event recording.
461    pub fn with_recording(mut self) -> Self {
462        self.recording = true;
463        self
464    }
465
466    /// Build the test context.
467    pub fn build(self) -> Result<TestContext> {
468        let trace_id = self.trace_id.unwrap_or_else(TraceId::new);
469        let node_id = self.node_id.unwrap_or_else(|| NodeId::new(0));
470
471        // Create arena
472        let arena = crate::arena::Arena::create(trace_id, &self.arena_config)?;
473        let reader = arena.reader();
474        let writer = arena.writer();
475
476        // Create WAL
477        let wal = Arc::new(Wal::open(self.wal_config.clone())?);
478
479        // Default providers
480        let clock = self.clock.unwrap_or_else(|| Arc::new(MockClock::new()));
481        let http = self.http.unwrap_or_else(|| Arc::new(MockHttp::new()));
482        let rng = self.rng.unwrap_or_else(|| Arc::new(MockRng::seeded(0)));
483        let uuid = self
484            .uuid
485            .unwrap_or_else(|| Arc::new(MockUuid::sequential()));
486        let fs = self.fs.unwrap_or_else(|| Arc::new(MockFs::new()));
487        let env = self.env.unwrap_or_else(|| Arc::new(MockEnv::new()));
488        let secrets = self.secrets.unwrap_or_else(|| Arc::new(MockSecrets::new()));
489
490        let recorder = if self.recording {
491            Some(Arc::new(EventRecorder::new()))
492        } else {
493            None
494        };
495
496        Ok(TestContext {
497            trace_id,
498            node_id,
499            reader,
500            writer,
501            wal,
502
503            clock,
504            http,
505            rng,
506            uuid,
507            fs,
508            env,
509            secrets,
510
511            recorder,
512        })
513    }
514}
515
516impl Default for TestContextBuilder {
517    fn default() -> Self {
518        Self::new()
519    }
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525
526    #[test]
527    fn build_default_context() {
528        let ctx = TestContextBuilder::new().build().unwrap();
529
530        // Verify defaults are mocks
531        assert!(ctx.clock.is_mock());
532        assert!(ctx.http.is_mock());
533        assert!(ctx.rng.is_mock());
534        assert!(ctx.uuid.is_mock());
535        assert!(ctx.fs.is_mock());
536        assert!(ctx.env.is_mock());
537        assert!(ctx.secrets.is_mock());
538    }
539
540    #[test]
541    fn build_with_fixed_time() {
542        let ctx = TestContextBuilder::new()
543            .with_fixed_time("2024-01-15T10:30:00Z")
544            .build()
545            .unwrap();
546
547        // 2024-01-15T10:30:00Z = 1705314600000 ms since epoch
548        assert_eq!(ctx.system_time_millis(), 1705314600000);
549    }
550
551    #[test]
552    fn build_with_seed() {
553        let ctx1 = TestContextBuilder::new().with_seed(42).build().unwrap();
554        let ctx2 = TestContextBuilder::new().with_seed(42).build().unwrap();
555
556        let v1 = ctx1.random_u64();
557        let v2 = ctx2.random_u64();
558
559        assert_eq!(v1, v2);
560    }
561
562    #[test]
563    fn build_with_sequential_uuids() {
564        let ctx = TestContextBuilder::new()
565            .with_sequential_uuids()
566            .build()
567            .unwrap();
568
569        let id1 = ctx.new_uuid();
570        let id2 = ctx.new_uuid();
571
572        assert_eq!(id1.to_string(), "00000000-0000-0000-0000-000000000001");
573        assert_eq!(id2.to_string(), "00000000-0000-0000-0000-000000000002");
574    }
575
576    #[test]
577    fn build_with_env_vars() {
578        let ctx = TestContextBuilder::new()
579            .with_env_vars(&[("FOO", "bar"), ("BAZ", "qux")])
580            .build()
581            .unwrap();
582
583        assert_eq!(ctx.env_var("FOO"), Some("bar".to_string()));
584        assert_eq!(ctx.env_var("BAZ"), Some("qux".to_string()));
585        assert_eq!(ctx.env_var("MISSING"), None);
586    }
587
588    #[test]
589    fn build_with_secrets() {
590        let ctx = TestContextBuilder::new()
591            .with_secrets(&[("API_KEY", "secret-123")])
592            .build()
593            .unwrap();
594
595        assert_eq!(ctx.secret("API_KEY"), Some("secret-123".to_string()));
596        assert_eq!(ctx.secret("MISSING"), None);
597    }
598
599    #[test]
600    fn build_with_recording() {
601        let ctx = TestContextBuilder::new().with_recording().build().unwrap();
602
603        // Generate some events
604        ctx.now();
605        ctx.new_uuid();
606        ctx.random_u64();
607
608        let recorder = ctx.recorder().unwrap();
609        assert_eq!(recorder.len(), 3);
610        assert!(recorder.assert_recorded("clock_now"));
611        assert!(recorder.assert_recorded("uuid_generated"));
612        assert!(recorder.assert_recorded("random_u64"));
613    }
614
615    #[test]
616    fn arena_operations() {
617        let ctx = TestContextBuilder::new().build().unwrap();
618
619        let data = b"hello world";
620        let ptr = ctx.write_bytes(data).unwrap();
621
622        let read_data = ctx.read_bytes(ptr).unwrap();
623        assert_eq!(read_data, data);
624    }
625}