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