Skip to main content

eventcore_testing/
event_collector.rs

1//! Test utility for collecting events during projection for assertions.
2//!
3//! `EventCollector` implements the `Projector` trait and accumulates events
4//! in an `Arc<Mutex<Vec<E>>>` for shared access during testing. This allows
5//! test code to verify that commands produced expected events by running
6//! a projection and inspecting the collected results.
7//!
8//! # Example
9//!
10//! ```ignore
11//! use std::sync::{Arc, Mutex};
12//!
13//! execute(&mut store, &command).await?;
14//!
15//! let storage = Arc::new(Mutex::new(Vec::new()));
16//! let collector = EventCollector::<MyEvent>::new(storage.clone());
17//! run_projection(collector, &backend).await?;
18//!
19//! // Events accessible through the original storage handle
20//! assert_eq!(storage.lock().unwrap().len(), expected_count);
21//! ```
22
23use eventcore_types::{Projector, StreamPosition};
24use std::convert::Infallible;
25use std::sync::{Arc, Mutex};
26
27/// A projector that collects events for testing assertions.
28///
29/// `EventCollector` stores events in shared, thread-safe storage (`Arc<Mutex<Vec<E>>>`)
30/// so that events can be inspected after projection completes. This is the primary
31/// mechanism for black-box integration testing in EventCore.
32///
33/// # Type Parameters
34///
35/// - `E`: The event type to collect. Must be `Clone` so that `events()` can return
36///   owned copies without consuming the collector.
37///
38/// # Thread Safety
39///
40/// The internal storage uses `Arc<Mutex<_>>` to allow the collector to be shared
41/// across threads (e.g., between the projection runner and test assertions).
42#[derive(Debug)]
43pub struct EventCollector<E> {
44    events: Arc<Mutex<Vec<E>>>,
45}
46
47impl<E> EventCollector<E> {
48    /// Creates a new `EventCollector` with the provided shared storage.
49    ///
50    /// # Arguments
51    ///
52    /// * `storage` - An `Arc<Mutex<Vec<E>>>` that will hold collected events.
53    ///   The same storage can be cloned before passing to enable access to
54    ///   collected events after the collector is moved.
55    pub fn new(storage: Arc<Mutex<Vec<E>>>) -> Self {
56        Self { events: storage }
57    }
58
59    /// Returns a clone of all collected events.
60    ///
61    /// This method clones the internal vector, allowing inspection without
62    /// consuming the collector. The `Clone` bound on `E` enables this behavior.
63    pub fn events(&self) -> Vec<E>
64    where
65        E: Clone,
66    {
67        self.events
68            .lock()
69            .expect("EventCollector mutex poisoned - a test panicked while holding the lock")
70            .clone()
71    }
72}
73
74impl<E: Send + 'static> Projector for EventCollector<E> {
75    type Event = E;
76    type Error = Infallible;
77    type Context = ();
78
79    fn apply(
80        &mut self,
81        event: Self::Event,
82        _position: StreamPosition,
83        _ctx: &mut Self::Context,
84    ) -> Result<(), Self::Error> {
85        self.events
86            .lock()
87            .expect("EventCollector mutex poisoned - a test panicked while holding the lock")
88            .push(event);
89        Ok(())
90    }
91
92    fn name(&self) -> &str {
93        "event-collector"
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use crate::event_collector::EventCollector;
100
101    // Simple test event for unit tests
102    #[derive(Debug, Clone, PartialEq)]
103    struct TestEvent {
104        id: u32,
105    }
106
107    #[test]
108    fn new_collector_has_empty_events() {
109        use std::sync::{Arc, Mutex};
110
111        // Given: A newly created EventCollector
112        let storage: Arc<Mutex<Vec<TestEvent>>> = Arc::new(Mutex::new(Vec::new()));
113        let collector = EventCollector::new(storage);
114
115        // When: We retrieve the events
116        let events = collector.events();
117
118        // Then: The events vector is empty
119        assert!(events.is_empty());
120    }
121
122    #[test]
123    fn collects_event_via_projector_apply() {
124        use eventcore_types::{Projector, StreamPosition};
125        use std::sync::{Arc, Mutex};
126        use uuid::Uuid;
127
128        // Given: An EventCollector
129        let storage: Arc<Mutex<Vec<TestEvent>>> = Arc::new(Mutex::new(Vec::new()));
130        let mut collector = EventCollector::new(storage);
131        let event = TestEvent { id: 42 };
132        let position = StreamPosition::new(Uuid::nil());
133
134        // When: We apply an event via the Projector trait
135        let result = collector.apply(event.clone(), position, &mut ());
136
137        // Then: The apply succeeded and the event is collected
138        assert!(result.is_ok());
139        assert_eq!(collector.events(), vec![event]);
140    }
141
142    #[test]
143    fn events_accessible_after_collector_moved() {
144        use eventcore_types::{Projector, StreamPosition};
145        use std::sync::{Arc, Mutex};
146        use uuid::Uuid;
147
148        // Given: Shared storage and a collector using that storage
149        let storage: Arc<Mutex<Vec<TestEvent>>> = Arc::new(Mutex::new(Vec::new()));
150        let collector = EventCollector::new(storage.clone());
151
152        // When: Collector is moved (simulates move into run_projection) and events are applied
153        let mut moved_collector = collector;
154        let event = TestEvent { id: 99 };
155        let position = StreamPosition::new(Uuid::nil());
156        let _ = moved_collector.apply(event.clone(), position, &mut ());
157
158        // Then: Events are accessible through the original storage handle
159        let events = storage.lock().unwrap();
160        assert_eq!(*events, vec![event]);
161    }
162
163    #[test]
164    fn projector_name_is_event_collector() {
165        use eventcore_types::Projector;
166        use std::sync::{Arc, Mutex};
167
168        // Given: An EventCollector
169        let storage: Arc<Mutex<Vec<TestEvent>>> = Arc::new(Mutex::new(Vec::new()));
170        let collector = EventCollector::new(storage);
171
172        // When/Then: The projector name is "event-collector"
173        assert_eq!(collector.name(), "event-collector");
174    }
175}