hojicha_runtime/testing/
test_runner.rs

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