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}