hojicha_runtime/testing/
test_runner.rs

1//! Test runner utilities for testing hojicha applications
2
3use crate::program::{Program, ProgramOptions};
4use hojicha_core::{core::Model, error::Result, event::Event};
5use std::sync::{mpsc, Arc, Mutex};
6use std::time::Duration;
7
8/// A test runner that provides utilities for testing hojicha applications
9pub struct TestRunner<M: Model> {
10    program: crate::program::Program<M>,
11    timeout: Option<Duration>,
12    _message_log: Arc<Mutex<Vec<Event<M::Message>>>>,
13}
14
15impl<M: Model> TestRunner<M>
16where
17    M::Message: Clone + Send + 'static,
18{
19    /// Create a new test runner with the given model
20    pub fn new(model: M) -> Result<Self> {
21        Self::with_options(model, ProgramOptions::default().headless())
22    }
23
24    /// Create a new test runner with custom options
25    pub fn with_options(model: M, options: ProgramOptions) -> Result<Self> {
26        let program = Program::with_options(model, options)?;
27        Ok(Self {
28            program,
29            timeout: Some(Duration::from_secs(5)), // Default 5 second timeout
30            _message_log: Arc::new(Mutex::new(Vec::new())),
31        })
32    }
33
34    /// Set the timeout for test execution
35    pub fn with_timeout(mut self, timeout: Duration) -> Self {
36        self.timeout = Some(timeout);
37        self
38    }
39
40    /// Run without any timeout (be careful - may hang!)
41    pub fn without_timeout(mut self) -> Self {
42        self.timeout = None;
43        self
44    }
45
46    /// Initialize the async bridge and return a sender
47    pub fn init_async_bridge(&mut self) -> mpsc::SyncSender<Event<M::Message>> {
48        self.program.init_async_bridge()
49    }
50
51    /// Get a sender if async bridge was initialized
52    pub fn sender(&self) -> Option<mpsc::SyncSender<Event<M::Message>>> {
53        self.program.sender()
54    }
55
56    /// Send a message to the program
57    pub fn send_message(&self, msg: M::Message) -> Result<()> {
58        self.program.send_message(msg)
59    }
60
61    /// Run the program with the configured timeout
62    pub fn run(self) -> Result<()> {
63        match self.timeout {
64            Some(timeout) => self.program.run_with_timeout(timeout),
65            None => self.program.run(),
66        }
67    }
68
69    /// Run until a condition is met
70    pub fn run_until<F>(self, condition: F) -> Result<()>
71    where
72        F: FnMut(&M) -> bool + 'static,
73    {
74        self.program.run_until(condition)
75    }
76
77    /// Run for a specific number of update cycles
78    pub fn run_for_updates(self, count: usize) -> Result<()> {
79        let counter = Arc::new(Mutex::new(0));
80        let target = count;
81
82        self.program.run_until(move |_| {
83            let mut c = counter.lock().unwrap();
84            *c += 1;
85            *c >= target
86        })
87    }
88}
89
90/// Macro to simplify test setup
91#[macro_export]
92macro_rules! test_runner {
93    ($model:expr) => {
94        $crate::testing::TestRunner::new($model).unwrap()
95    };
96    ($model:expr, $timeout:expr) => {
97        $crate::testing::TestRunner::new($model)
98            .unwrap()
99            .with_timeout($timeout)
100    };
101}