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