Skip to main content

reifydb_runtime/actor/
testing.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (c) 2025 ReifyDB
3
4//! Testing utilities for actors.
5//!
6//! This module provides a [`TestHarness`] for synchronous actor testing
7//! without spawning actual tasks or threads.
8//!
9//! # Example
10//!
11//! ```
12//! #[test]
13//! fn test_counter() {
14//! 	struct Counter;
15//!
16//! 	impl Actor for Counter {
17//! 		type State = i64;
18//! 		type Message = i64;
19//!
20//! 		fn init(&self, _ctx: &Context<Self::Message>) -> Self::State {
21//! 			0
22//! 		}
23//!
24//! 		fn handle(
25//! 			&self,
26//! 			state: &mut Self::State,
27//! 			msg: Self::Message,
28//! 			_ctx: &Context<Self::Message>,
29//! 		) -> Directive {
30//! 			*state += msg;
31//! 			Directive::Continue
32//! 		}
33//! 	}
34//!
35//! 	let mut harness = TestHarness::new(Counter);
36//! 	harness.send(5);
37//! 	harness.send(3);
38//! 	harness.process_all();
39//!
40//! 	assert_eq!(*harness.state(), 8);
41//! }
42//! ```
43
44use std::{collections::VecDeque, marker::PhantomData};
45
46#[cfg(reifydb_target = "native")]
47use crossbeam_channel::unbounded;
48
49#[cfg(reifydb_target = "native")]
50use crate::actor::mailbox::ActorRef;
51#[cfg(reifydb_target = "wasm")]
52use crate::actor::mailbox::create_actor_ref;
53use crate::{
54	SharedRuntimeConfig,
55	actor::{
56		context::{CancellationToken, Context},
57		system::ActorSystem,
58		traits::{Actor, Directive},
59	},
60};
61
62/// Test harness for synchronous actor testing.
63///
64/// This harness allows testing actors without spawning tasks:
65/// - Messages are queued in a local VecDeque
66/// - Processing is explicit via `process_one()` or `process_all()`
67/// - State is directly accessible for assertions
68pub struct TestHarness<A: Actor> {
69	actor: A,
70	state: A::State,
71	mailbox: VecDeque<A::Message>,
72	ctx: TestContext<A::Message>,
73}
74
75impl<A: Actor> TestHarness<A> {
76	/// Create a new test harness for the given actor.
77	pub fn new(actor: A) -> Self {
78		let ctx = TestContext::new();
79		let state = actor.init(&ctx.to_context());
80
81		Self {
82			actor,
83			state,
84			mailbox: VecDeque::new(),
85			ctx,
86		}
87	}
88
89	/// Create a new test harness with a pre-initialized state.
90	///
91	/// This is useful when you want to test specific state transitions
92	/// without going through the init process.
93	pub fn with_state(actor: A, state: A::State) -> Self {
94		let ctx = TestContext::new();
95
96		Self {
97			actor,
98			state,
99			mailbox: VecDeque::new(),
100			ctx,
101		}
102	}
103
104	/// Send a message to the actor's mailbox.
105	///
106	/// The message will be queued and processed when `process_one()`
107	/// or `process_all()` is called.
108	pub fn send(&mut self, msg: A::Message) {
109		self.mailbox.push_back(msg);
110	}
111
112	/// Process a single message from the mailbox.
113	///
114	/// Returns `Some(flow)` if a message was processed,
115	/// or `None` if the mailbox was empty.
116	pub fn process_one(&mut self) -> Option<Directive> {
117		let msg = self.mailbox.pop_front()?;
118		let flow = self.actor.handle(&mut self.state, msg, &self.ctx.to_context());
119		Some(flow)
120	}
121
122	/// Process all messages in the mailbox.
123	///
124	/// Returns a Vec of all Directive values returned by handle().
125	/// Processing stops early if any handler returns `Directive::Stop`.
126	pub fn process_all(&mut self) -> Vec<Directive> {
127		let mut flows = Vec::new();
128
129		while let Some(flow) = self.process_one() {
130			flows.push(flow);
131			if flow == Directive::Stop {
132				break;
133			}
134		}
135
136		flows
137	}
138
139	/// Process messages until the mailbox is empty or a condition is met.
140	///
141	/// Returns the flows from all processed messages.
142	pub fn process_until<F>(&mut self, mut condition: F) -> Vec<Directive>
143	where
144		F: FnMut(&A::State) -> bool,
145	{
146		let mut flows = Vec::new();
147
148		while !self.mailbox.is_empty() {
149			if condition(&self.state) {
150				break;
151			}
152
153			if let Some(flow) = self.process_one() {
154				flows.push(flow);
155				if flow == Directive::Stop {
156					break;
157				}
158			}
159		}
160
161		flows
162	}
163
164	/// Call the actor's idle hook.
165	///
166	/// This is useful for testing background work behavior.
167	pub fn idle(&mut self) -> Directive {
168		self.actor.idle(&self.ctx.to_context())
169	}
170
171	/// Call the actor's post_stop hook.
172	pub fn post_stop(&mut self) {
173		self.actor.post_stop();
174	}
175
176	/// Get a reference to the actor's state.
177	pub fn state(&self) -> &A::State {
178		&self.state
179	}
180
181	/// Get a mutable reference to the actor's state.
182	pub fn state_mut(&mut self) -> &mut A::State {
183		&mut self.state
184	}
185
186	/// Check if the mailbox is empty.
187	pub fn is_empty(&self) -> bool {
188		self.mailbox.is_empty()
189	}
190
191	/// Get the number of messages in the mailbox.
192	pub fn mailbox_len(&self) -> usize {
193		self.mailbox.len()
194	}
195
196	/// Signal cancellation.
197	pub fn cancel(&mut self) {
198		self.ctx.cancel();
199	}
200
201	/// Check if cancelled.
202	pub fn is_cancelled(&self) -> bool {
203		self.ctx.is_cancelled()
204	}
205}
206
207/// Test context that doesn't require a real runtime.
208struct TestContext<M> {
209	cancel: CancellationToken,
210	_marker: PhantomData<M>,
211}
212
213impl<M: Send + 'static> TestContext<M> {
214	fn new() -> Self {
215		Self {
216			cancel: CancellationToken::new(),
217			_marker: PhantomData,
218		}
219	}
220
221	fn cancel(&self) {
222		self.cancel.cancel();
223	}
224
225	fn is_cancelled(&self) -> bool {
226		self.cancel.is_cancelled()
227	}
228
229	/// Convert to a Context.
230	///
231	/// Note: The ActorRef in this context is not usable for sending
232	/// messages in tests. Use `harness.send()` instead.
233	fn to_context(&self) -> Context<M> {
234		// Create a dummy actor ref using platform-specific implementation
235		#[cfg(reifydb_target = "native")]
236		let actor_ref = {
237			let (tx, _rx) = unbounded();
238			ActorRef::new(tx)
239		};
240
241		#[cfg(reifydb_target = "wasm")]
242		let actor_ref = create_actor_ref();
243
244		// Create an actor system for testing
245		let system = ActorSystem::new(SharedRuntimeConfig::default().actor_system_config());
246
247		Context::new(actor_ref, system, self.cancel.clone())
248	}
249}
250
251#[cfg(test)]
252mod tests {
253	use super::*;
254
255	struct CounterActor;
256
257	impl Actor for CounterActor {
258		type State = i64;
259		type Message = CounterMsg;
260
261		fn init(&self, _ctx: &Context<Self::Message>) -> Self::State {
262			0
263		}
264
265		fn handle(
266			&self,
267			state: &mut Self::State,
268			msg: Self::Message,
269			_ctx: &Context<Self::Message>,
270		) -> Directive {
271			match msg {
272				CounterMsg::Inc => *state += 1,
273				CounterMsg::Dec => *state -= 1,
274				CounterMsg::Set(v) => *state = v,
275				CounterMsg::Stop => return Directive::Stop,
276			}
277			Directive::Continue
278		}
279
280		fn idle(&self, _ctx: &Context<Self::Message>) -> Directive {
281			Directive::Park
282		}
283	}
284
285	#[derive(Debug)]
286	enum CounterMsg {
287		Inc,
288		Dec,
289		Set(i64),
290		Stop,
291	}
292
293	#[test]
294	fn test_harness_basic() {
295		let mut harness = TestHarness::new(CounterActor);
296
297		harness.send(CounterMsg::Inc);
298		harness.send(CounterMsg::Inc);
299		harness.send(CounterMsg::Inc);
300
301		assert_eq!(harness.mailbox_len(), 3);
302
303		let flows = harness.process_all();
304
305		assert_eq!(flows.len(), 3);
306		assert!(flows.iter().all(|f| *f == Directive::Continue));
307		assert_eq!(*harness.state(), 3);
308	}
309
310	#[test]
311	fn test_harness_stops_on_stop() {
312		let mut harness = TestHarness::new(CounterActor);
313
314		harness.send(CounterMsg::Inc);
315		harness.send(CounterMsg::Stop);
316		harness.send(CounterMsg::Inc); // Should not be processed
317
318		let flows = harness.process_all();
319
320		assert_eq!(flows.len(), 2);
321		assert_eq!(flows[1], Directive::Stop);
322		assert_eq!(*harness.state(), 1);
323		assert_eq!(harness.mailbox_len(), 1); // One message left
324	}
325
326	#[test]
327	fn test_harness_process_one() {
328		let mut harness = TestHarness::new(CounterActor);
329
330		harness.send(CounterMsg::Set(42));
331		harness.send(CounterMsg::Inc);
332
333		assert_eq!(harness.process_one(), Some(Directive::Continue));
334		assert_eq!(*harness.state(), 42);
335
336		assert_eq!(harness.process_one(), Some(Directive::Continue));
337		assert_eq!(*harness.state(), 43);
338
339		assert_eq!(harness.process_one(), None);
340	}
341
342	#[test]
343	fn test_harness_idle() {
344		let mut harness = TestHarness::new(CounterActor);
345
346		let flow = harness.idle();
347		assert_eq!(flow, Directive::Park);
348	}
349}