umi_memory/storage/
sim.rs

1//! SimStorageBackend - In-Memory Storage for Testing
2//!
3//! TigerStyle: Deterministic testing with fault injection.
4//!
5//! # Simulation-First
6//!
7//! This file follows simulation-first development:
8//! 1. Tests are written FIRST (below)
9//! 2. Implementation follows to make tests pass
10//! 3. DST integration enables fault injection
11
12use std::collections::HashMap;
13use std::sync::{Arc, RwLock};
14
15use async_trait::async_trait;
16
17use crate::dst::{DeterministicRng, FaultConfig, FaultInjector, SimClock, SimConfig};
18
19use super::backend::StorageBackend;
20use super::entity::{Entity, EntityType};
21use super::error::{StorageError, StorageResult};
22
23// =============================================================================
24// SimStorageBackend
25// =============================================================================
26
27/// In-memory storage backend for testing.
28///
29/// TigerStyle:
30/// - Deterministic via SimClock and DeterministicRng
31/// - Fault injection via FaultInjector
32/// - Thread-safe with RwLock
33#[derive(Debug, Clone)]
34pub struct SimStorageBackend {
35    /// Stored entities indexed by ID
36    storage: Arc<RwLock<HashMap<String, Entity>>>,
37    /// Fault injector for simulating failures
38    fault_injector: Arc<FaultInjector>,
39    /// Simulated clock
40    clock: SimClock,
41    /// Deterministic RNG for operations
42    #[allow(dead_code)]
43    rng: Arc<RwLock<DeterministicRng>>,
44}
45
46impl SimStorageBackend {
47    /// Create a new SimStorageBackend with given config.
48    #[must_use]
49    pub fn new(config: SimConfig) -> Self {
50        let mut rng = DeterministicRng::new(config.seed());
51        let fault_rng = rng.fork();
52
53        Self {
54            storage: Arc::new(RwLock::new(HashMap::new())),
55            fault_injector: Arc::new(FaultInjector::new(fault_rng)),
56            clock: SimClock::new(),
57            rng: Arc::new(RwLock::new(rng)),
58        }
59    }
60
61    /// Add fault configuration.
62    ///
63    /// Note: Creates a new backend with the fault registered.
64    /// FaultInjector uses interior mutability, but register needs &mut self
65    /// which we can't do through Arc. So we create with faults upfront.
66    #[must_use]
67    pub fn with_faults(mut self, config: FaultConfig) -> Self {
68        // Get mutable access before wrapping in Arc
69        // This only works because we haven't shared the Arc yet
70        Arc::get_mut(&mut self.fault_injector)
71            .expect("cannot add faults after backend is shared")
72            .register(config);
73        self
74    }
75
76    /// Get the simulated clock.
77    #[must_use]
78    pub fn clock(&self) -> &SimClock {
79        &self.clock
80    }
81
82    /// Get fault injector for inspection.
83    #[must_use]
84    pub fn fault_injector(&self) -> &Arc<FaultInjector> {
85        &self.fault_injector
86    }
87
88    /// Check if a fault should be injected for an operation.
89    fn maybe_inject_fault(&self, operation: &str) -> StorageResult<()> {
90        if let Some(fault_type) = self.fault_injector.should_inject(operation) {
91            Err(StorageError::simulated_fault(format!(
92                "{:?} during {}",
93                fault_type, operation
94            )))
95        } else {
96            Ok(())
97        }
98    }
99
100    /// Get entity count (for testing).
101    #[must_use]
102    pub fn entity_count(&self) -> usize {
103        self.storage.read().unwrap().len()
104    }
105}
106
107#[async_trait]
108impl StorageBackend for SimStorageBackend {
109    async fn store_entity(&self, entity: &Entity) -> StorageResult<String> {
110        // Check for faults
111        self.maybe_inject_fault("store")?;
112
113        // Preconditions
114        assert!(!entity.id.is_empty(), "entity must have id");
115
116        let mut storage = self.storage.write().unwrap();
117        storage.insert(entity.id.clone(), entity.clone());
118
119        Ok(entity.id.clone())
120    }
121
122    async fn get_entity(&self, id: &str) -> StorageResult<Option<Entity>> {
123        // Check for faults
124        self.maybe_inject_fault("get")?;
125
126        let storage = self.storage.read().unwrap();
127        Ok(storage.get(id).cloned())
128    }
129
130    async fn delete_entity(&self, id: &str) -> StorageResult<bool> {
131        // Check for faults
132        self.maybe_inject_fault("delete")?;
133
134        let mut storage = self.storage.write().unwrap();
135        Ok(storage.remove(id).is_some())
136    }
137
138    async fn search(&self, query: &str, limit: usize) -> StorageResult<Vec<Entity>> {
139        // Check for faults
140        self.maybe_inject_fault("search")?;
141
142        let storage = self.storage.read().unwrap();
143        let query_lower = query.to_lowercase();
144
145        let mut results: Vec<Entity> = storage
146            .values()
147            .filter(|e| {
148                e.name.to_lowercase().contains(&query_lower)
149                    || e.content.to_lowercase().contains(&query_lower)
150            })
151            .cloned()
152            .collect();
153
154        // Sort by name for determinism
155        results.sort_by(|a, b| a.name.cmp(&b.name));
156
157        // Apply limit
158        results.truncate(limit);
159
160        Ok(results)
161    }
162
163    async fn list_entities(
164        &self,
165        entity_type: Option<EntityType>,
166        limit: usize,
167        offset: usize,
168    ) -> StorageResult<Vec<Entity>> {
169        // Check for faults
170        self.maybe_inject_fault("list")?;
171
172        let storage = self.storage.read().unwrap();
173
174        let mut results: Vec<Entity> = storage
175            .values()
176            .filter(|e| entity_type.map_or(true, |t| e.entity_type == t))
177            .cloned()
178            .collect();
179
180        // Sort by created_at for determinism
181        results.sort_by(|a, b| a.created_at.cmp(&b.created_at));
182
183        // Apply offset and limit
184        let results: Vec<Entity> = results.into_iter().skip(offset).take(limit).collect();
185
186        Ok(results)
187    }
188
189    async fn count_entities(&self, entity_type: Option<EntityType>) -> StorageResult<usize> {
190        // Check for faults
191        self.maybe_inject_fault("count")?;
192
193        let storage = self.storage.read().unwrap();
194
195        let count = storage
196            .values()
197            .filter(|e| entity_type.map_or(true, |t| e.entity_type == t))
198            .count();
199
200        Ok(count)
201    }
202
203    async fn clear(&self) -> StorageResult<()> {
204        // Check for faults
205        self.maybe_inject_fault("clear")?;
206
207        let mut storage = self.storage.write().unwrap();
208        storage.clear();
209        Ok(())
210    }
211}
212
213// =============================================================================
214// TESTS - Written FIRST (Simulation-First)
215// =============================================================================
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    // =========================================================================
222    // Basic CRUD Tests
223    // =========================================================================
224
225    #[tokio::test]
226    async fn test_store_and_get() {
227        let backend = SimStorageBackend::new(SimConfig::with_seed(42));
228
229        let entity = Entity::new(
230            EntityType::Person,
231            "Alice".to_string(),
232            "Friend".to_string(),
233        );
234        let id = entity.id.clone();
235
236        backend.store_entity(&entity).await.unwrap();
237
238        let retrieved = backend.get_entity(&id).await.unwrap();
239        assert!(retrieved.is_some());
240
241        let retrieved = retrieved.unwrap();
242        assert_eq!(retrieved.id, id);
243        assert_eq!(retrieved.name, "Alice");
244        assert_eq!(retrieved.entity_type, EntityType::Person);
245    }
246
247    #[tokio::test]
248    async fn test_get_nonexistent() {
249        let backend = SimStorageBackend::new(SimConfig::with_seed(42));
250
251        let result = backend.get_entity("nonexistent").await.unwrap();
252        assert!(result.is_none());
253    }
254
255    #[tokio::test]
256    async fn test_store_updates_existing() {
257        let backend = SimStorageBackend::new(SimConfig::with_seed(42));
258
259        let mut entity = Entity::new(EntityType::Note, "Note".to_string(), "Original".to_string());
260        let id = entity.id.clone();
261
262        backend.store_entity(&entity).await.unwrap();
263
264        // Update content
265        entity.update_content("Updated".to_string());
266        backend.store_entity(&entity).await.unwrap();
267
268        let retrieved = backend.get_entity(&id).await.unwrap().unwrap();
269        assert_eq!(retrieved.content, "Updated");
270
271        // Should still be just one entity
272        assert_eq!(backend.entity_count(), 1);
273    }
274
275    #[tokio::test]
276    async fn test_delete() {
277        let backend = SimStorageBackend::new(SimConfig::with_seed(42));
278
279        let entity = Entity::new(EntityType::Task, "Task".to_string(), "Do it".to_string());
280        let id = entity.id.clone();
281
282        backend.store_entity(&entity).await.unwrap();
283        assert_eq!(backend.entity_count(), 1);
284
285        let deleted = backend.delete_entity(&id).await.unwrap();
286        assert!(deleted);
287        assert_eq!(backend.entity_count(), 0);
288
289        // Deleting again returns false
290        let deleted = backend.delete_entity(&id).await.unwrap();
291        assert!(!deleted);
292    }
293
294    #[tokio::test]
295    async fn test_clear() {
296        let backend = SimStorageBackend::new(SimConfig::with_seed(42));
297
298        backend
299            .store_entity(&Entity::new(
300                EntityType::Note,
301                "A".to_string(),
302                "a".to_string(),
303            ))
304            .await
305            .unwrap();
306        backend
307            .store_entity(&Entity::new(
308                EntityType::Note,
309                "B".to_string(),
310                "b".to_string(),
311            ))
312            .await
313            .unwrap();
314
315        assert_eq!(backend.entity_count(), 2);
316
317        backend.clear().await.unwrap();
318
319        assert_eq!(backend.entity_count(), 0);
320    }
321
322    // =========================================================================
323    // Search Tests
324    // =========================================================================
325
326    #[tokio::test]
327    async fn test_search_by_name() {
328        let backend = SimStorageBackend::new(SimConfig::with_seed(42));
329
330        backend
331            .store_entity(&Entity::new(
332                EntityType::Person,
333                "Alice".to_string(),
334                "Friend".to_string(),
335            ))
336            .await
337            .unwrap();
338        backend
339            .store_entity(&Entity::new(
340                EntityType::Person,
341                "Bob".to_string(),
342                "Colleague".to_string(),
343            ))
344            .await
345            .unwrap();
346        backend
347            .store_entity(&Entity::new(
348                EntityType::Person,
349                "Charlie".to_string(),
350                "Neighbor".to_string(),
351            ))
352            .await
353            .unwrap();
354
355        let results = backend.search("alice", 10).await.unwrap();
356        assert_eq!(results.len(), 1);
357        assert_eq!(results[0].name, "Alice");
358    }
359
360    #[tokio::test]
361    async fn test_search_by_content() {
362        let backend = SimStorageBackend::new(SimConfig::with_seed(42));
363
364        backend
365            .store_entity(&Entity::new(
366                EntityType::Note,
367                "Work".to_string(),
368                "Meeting with team".to_string(),
369            ))
370            .await
371            .unwrap();
372        backend
373            .store_entity(&Entity::new(
374                EntityType::Note,
375                "Personal".to_string(),
376                "Call mom".to_string(),
377            ))
378            .await
379            .unwrap();
380
381        let results = backend.search("meeting", 10).await.unwrap();
382        assert_eq!(results.len(), 1);
383        assert_eq!(results[0].name, "Work");
384    }
385
386    #[tokio::test]
387    async fn test_search_case_insensitive() {
388        let backend = SimStorageBackend::new(SimConfig::with_seed(42));
389
390        backend
391            .store_entity(&Entity::new(
392                EntityType::Topic,
393                "Rust".to_string(),
394                "Programming language".to_string(),
395            ))
396            .await
397            .unwrap();
398
399        let results = backend.search("RUST", 10).await.unwrap();
400        assert_eq!(results.len(), 1);
401
402        let results = backend.search("rust", 10).await.unwrap();
403        assert_eq!(results.len(), 1);
404    }
405
406    #[tokio::test]
407    async fn test_search_limit() {
408        let backend = SimStorageBackend::new(SimConfig::with_seed(42));
409
410        for i in 0..10 {
411            backend
412                .store_entity(&Entity::new(
413                    EntityType::Note,
414                    format!("Note {}", i),
415                    "common content".to_string(),
416                ))
417                .await
418                .unwrap();
419        }
420
421        let results = backend.search("common", 3).await.unwrap();
422        assert_eq!(results.len(), 3);
423    }
424
425    // =========================================================================
426    // List Tests
427    // =========================================================================
428
429    #[tokio::test]
430    async fn test_list_all() {
431        let backend = SimStorageBackend::new(SimConfig::with_seed(42));
432
433        backend
434            .store_entity(&Entity::new(
435                EntityType::Person,
436                "Alice".to_string(),
437                "a".to_string(),
438            ))
439            .await
440            .unwrap();
441        backend
442            .store_entity(&Entity::new(
443                EntityType::Project,
444                "Umi".to_string(),
445                "b".to_string(),
446            ))
447            .await
448            .unwrap();
449
450        let results = backend.list_entities(None, 100, 0).await.unwrap();
451        assert_eq!(results.len(), 2);
452    }
453
454    #[tokio::test]
455    async fn test_list_by_type() {
456        let backend = SimStorageBackend::new(SimConfig::with_seed(42));
457
458        backend
459            .store_entity(&Entity::new(
460                EntityType::Person,
461                "Alice".to_string(),
462                "a".to_string(),
463            ))
464            .await
465            .unwrap();
466        backend
467            .store_entity(&Entity::new(
468                EntityType::Person,
469                "Bob".to_string(),
470                "b".to_string(),
471            ))
472            .await
473            .unwrap();
474        backend
475            .store_entity(&Entity::new(
476                EntityType::Project,
477                "Umi".to_string(),
478                "c".to_string(),
479            ))
480            .await
481            .unwrap();
482
483        let results = backend
484            .list_entities(Some(EntityType::Person), 100, 0)
485            .await
486            .unwrap();
487        assert_eq!(results.len(), 2);
488
489        let results = backend
490            .list_entities(Some(EntityType::Project), 100, 0)
491            .await
492            .unwrap();
493        assert_eq!(results.len(), 1);
494    }
495
496    #[tokio::test]
497    async fn test_list_with_offset() {
498        let backend = SimStorageBackend::new(SimConfig::with_seed(42));
499
500        for i in 0..5 {
501            // Small delay to ensure different timestamps
502            backend
503                .store_entity(&Entity::new(
504                    EntityType::Note,
505                    format!("Note {}", i),
506                    "content".to_string(),
507                ))
508                .await
509                .unwrap();
510        }
511
512        let all = backend.list_entities(None, 100, 0).await.unwrap();
513        assert_eq!(all.len(), 5);
514
515        let offset_2 = backend.list_entities(None, 100, 2).await.unwrap();
516        assert_eq!(offset_2.len(), 3);
517    }
518
519    #[tokio::test]
520    async fn test_count() {
521        let backend = SimStorageBackend::new(SimConfig::with_seed(42));
522
523        backend
524            .store_entity(&Entity::new(
525                EntityType::Person,
526                "Alice".to_string(),
527                "a".to_string(),
528            ))
529            .await
530            .unwrap();
531        backend
532            .store_entity(&Entity::new(
533                EntityType::Person,
534                "Bob".to_string(),
535                "b".to_string(),
536            ))
537            .await
538            .unwrap();
539        backend
540            .store_entity(&Entity::new(
541                EntityType::Task,
542                "Task".to_string(),
543                "c".to_string(),
544            ))
545            .await
546            .unwrap();
547
548        assert_eq!(backend.count_entities(None).await.unwrap(), 3);
549        assert_eq!(
550            backend
551                .count_entities(Some(EntityType::Person))
552                .await
553                .unwrap(),
554            2
555        );
556        assert_eq!(
557            backend
558                .count_entities(Some(EntityType::Task))
559                .await
560                .unwrap(),
561            1
562        );
563        assert_eq!(
564            backend
565                .count_entities(Some(EntityType::Project))
566                .await
567                .unwrap(),
568            0
569        );
570    }
571}
572
573// =============================================================================
574// DST Tests - Fault Injection
575// =============================================================================
576
577#[cfg(test)]
578mod dst_tests {
579    use super::*;
580    use crate::dst::FaultType;
581
582    #[tokio::test]
583    async fn test_fault_injection_on_store() {
584        let backend = SimStorageBackend::new(SimConfig::with_seed(42)).with_faults(
585            FaultConfig::new(FaultType::StorageWriteFail, 1.0) // Always fail
586                .with_filter("store"),
587        );
588
589        let entity = Entity::new(EntityType::Note, "Test".to_string(), "content".to_string());
590        let result = backend.store_entity(&entity).await;
591
592        assert!(result.is_err());
593        assert!(matches!(result, Err(StorageError::SimulatedFault { .. })));
594    }
595
596    #[tokio::test]
597    async fn test_fault_injection_on_get() {
598        let backend = SimStorageBackend::new(SimConfig::with_seed(42))
599            .with_faults(FaultConfig::new(FaultType::StorageReadFail, 1.0).with_filter("get"));
600
601        // Store should work (no fault on store)
602        let entity = Entity::new(EntityType::Note, "Test".to_string(), "content".to_string());
603        backend.store_entity(&entity).await.unwrap();
604
605        // Get should fail
606        let result = backend.get_entity(&entity.id).await;
607        assert!(result.is_err());
608    }
609
610    #[tokio::test]
611    async fn test_fault_injection_probability() {
612        // 50% fault probability
613        let backend = SimStorageBackend::new(SimConfig::with_seed(42))
614            .with_faults(FaultConfig::new(FaultType::StorageWriteFail, 0.5).with_filter("store"));
615
616        let mut successes = 0;
617        let mut failures = 0;
618
619        for i in 0..100 {
620            let entity = Entity::new(
621                EntityType::Note,
622                format!("Test {}", i),
623                "content".to_string(),
624            );
625            match backend.store_entity(&entity).await {
626                Ok(_) => successes += 1,
627                Err(_) => failures += 1,
628            }
629        }
630
631        // With 50% probability, we should see both successes and failures
632        // (statistically very unlikely to see all one or the other with 100 trials)
633        assert!(successes > 0, "expected some successes");
634        assert!(failures > 0, "expected some failures");
635    }
636
637    #[tokio::test]
638    async fn test_fault_injection_stats() {
639        let backend = SimStorageBackend::new(SimConfig::with_seed(42))
640            .with_faults(FaultConfig::new(FaultType::StorageWriteFail, 1.0).with_filter("store"));
641
642        let entity = Entity::new(EntityType::Note, "Test".to_string(), "content".to_string());
643
644        // Try 5 times
645        for _ in 0..5 {
646            let _ = backend.store_entity(&entity).await;
647        }
648
649        let total = backend.fault_injector().total_injections();
650        assert_eq!(total, 5);
651    }
652}
653
654// =============================================================================
655// Property-Based Tests
656// =============================================================================
657
658#[cfg(test)]
659mod property_tests {
660    use super::*;
661    use crate::dst::{PropertyTest, PropertyTestable, TimeAdvanceConfig};
662
663    /// Operations that can be performed on storage
664    #[derive(Debug, Clone)]
665    enum StorageOp {
666        Store {
667            entity_type: EntityType,
668            name: String,
669        },
670        Get {
671            id: String,
672        },
673        Delete {
674            id: String,
675        },
676        Search {
677            query: String,
678        },
679        List {
680            entity_type: Option<EntityType>,
681        },
682    }
683
684    /// Wrapper for property testing
685    struct StorageWrapper {
686        backend: SimStorageBackend,
687        known_ids: Vec<String>,
688    }
689
690    impl PropertyTestable for StorageWrapper {
691        type Operation = StorageOp;
692
693        fn generate_operation(&self, rng: &mut DeterministicRng) -> Self::Operation {
694            let op_type = rng.next_usize(0, 4);
695
696            match op_type {
697                0 => {
698                    // Store
699                    let types = EntityType::all();
700                    let type_idx = rng.next_usize(0, types.len() - 1);
701                    StorageOp::Store {
702                        entity_type: types[type_idx],
703                        name: format!("entity_{}", rng.next_usize(0, 999)),
704                    }
705                }
706                1 => {
707                    // Get
708                    let id = if !self.known_ids.is_empty() && rng.next_bool(0.7) {
709                        let idx = rng.next_usize(0, self.known_ids.len() - 1);
710                        self.known_ids[idx].clone()
711                    } else {
712                        format!("unknown_{}", rng.next_usize(0, 99))
713                    };
714                    StorageOp::Get { id }
715                }
716                2 => {
717                    // Delete
718                    let id = if !self.known_ids.is_empty() && rng.next_bool(0.5) {
719                        let idx = rng.next_usize(0, self.known_ids.len() - 1);
720                        self.known_ids[idx].clone()
721                    } else {
722                        format!("unknown_{}", rng.next_usize(0, 99))
723                    };
724                    StorageOp::Delete { id }
725                }
726                3 => {
727                    // Search
728                    StorageOp::Search {
729                        query: format!("entity_{}", rng.next_usize(0, 9)),
730                    }
731                }
732                _ => {
733                    // List
734                    let types = EntityType::all();
735                    let entity_type = if rng.next_bool(0.3) {
736                        Some(types[rng.next_usize(0, types.len() - 1)])
737                    } else {
738                        None
739                    };
740                    StorageOp::List { entity_type }
741                }
742            }
743        }
744
745        fn apply_operation(&mut self, op: &Self::Operation, _clock: &SimClock) {
746            let rt = tokio::runtime::Builder::new_current_thread()
747                .enable_all()
748                .build()
749                .unwrap();
750
751            rt.block_on(async {
752                match op {
753                    StorageOp::Store { entity_type, name } => {
754                        let entity = Entity::new(*entity_type, name.clone(), "content".to_string());
755                        if self.backend.store_entity(&entity).await.is_ok() {
756                            if !self.known_ids.contains(&entity.id) {
757                                self.known_ids.push(entity.id);
758                            }
759                        }
760                    }
761                    StorageOp::Get { id } => {
762                        let _ = self.backend.get_entity(id).await;
763                    }
764                    StorageOp::Delete { id } => {
765                        if self.backend.delete_entity(id).await.unwrap_or(false) {
766                            self.known_ids.retain(|i| i != id);
767                        }
768                    }
769                    StorageOp::Search { query } => {
770                        let _ = self.backend.search(query, 10).await;
771                    }
772                    StorageOp::List { entity_type } => {
773                        let _ = self.backend.list_entities(*entity_type, 10, 0).await;
774                    }
775                }
776            });
777        }
778
779        fn check_invariants(&self) -> Result<(), String> {
780            let rt = tokio::runtime::Builder::new_current_thread()
781                .enable_all()
782                .build()
783                .unwrap();
784
785            rt.block_on(async {
786                // Invariant 1: count matches known_ids (after filtering deleted)
787                let count = self
788                    .backend
789                    .count_entities(None)
790                    .await
791                    .map_err(|e| e.to_string())?;
792
793                // known_ids might include some that were deleted by parallel ops,
794                // so we just check count <= known_ids.len() doesn't hold (they should be close)
795                // Actually, we maintain known_ids properly, so count should equal known_ids.len()
796                if count != self.known_ids.len() {
797                    // This could happen if store failed silently - but we track correctly
798                    // So this is an invariant violation
799                    return Err(format!(
800                        "count {} != known_ids.len() {}",
801                        count,
802                        self.known_ids.len()
803                    ));
804                }
805
806                Ok(())
807            })
808        }
809
810        fn describe_state(&self) -> String {
811            format!(
812                "SimStorageBackend {{ entities: {}, known_ids: {} }}",
813                self.backend.entity_count(),
814                self.known_ids.len()
815            )
816        }
817    }
818
819    #[test]
820    fn test_property_invariants() {
821        let wrapper = StorageWrapper {
822            backend: SimStorageBackend::new(SimConfig::with_seed(42)),
823            known_ids: Vec::new(),
824        };
825
826        PropertyTest::new(42)
827            .with_max_operations(200)
828            .with_time_advance(TimeAdvanceConfig::none())
829            .run_and_assert(wrapper);
830    }
831
832    #[test]
833    fn test_property_multi_seed() {
834        for seed in [0, 1, 42, 12345, 99999] {
835            let wrapper = StorageWrapper {
836                backend: SimStorageBackend::new(SimConfig::with_seed(seed)),
837                known_ids: Vec::new(),
838            };
839
840            PropertyTest::new(seed)
841                .with_max_operations(100)
842                .with_time_advance(TimeAdvanceConfig::none())
843                .run_and_assert(wrapper);
844        }
845    }
846}