hojicha_runtime/testing/
async_test_utils.rs

1//! Utilities for testing async and timing-dependent behavior
2
3use hojicha_core::core::{Cmd, Message};
4use hojicha_core::event::Event;
5use std::sync::mpsc;
6use std::time::Duration;
7
8/// A test harness for async operations with controllable time
9pub struct AsyncTestHarness;
10
11impl Default for AsyncTestHarness {
12    fn default() -> Self {
13        Self::new()
14    }
15}
16
17impl AsyncTestHarness {
18    /// Create a new test harness
19    pub fn new() -> Self {
20        Self
21    }
22
23    /// Execute a command and collect all messages it produces
24    pub fn execute_command<M: Message + Clone + Send + 'static>(&self, cmd: Cmd<M>) -> Vec<M> {
25        let (tx, rx) = mpsc::sync_channel(100);
26
27        // Import CommandExecutor
28        use crate::program::CommandExecutor;
29        let executor = CommandExecutor::new().expect("Failed to create executor");
30
31        // Execute the command
32        executor.execute(cmd, &tx);
33
34        // Collect messages with a timeout
35        let mut messages = Vec::new();
36        let start = std::time::Instant::now();
37        let timeout = Duration::from_secs(1);
38
39        while start.elapsed() < timeout {
40            match rx.try_recv() {
41                Ok(Event::User(msg)) => messages.push(msg),
42                Ok(_) => {} // Ignore other events
43                Err(mpsc::TryRecvError::Empty) => {
44                    // Give async tasks time to complete
45                    std::thread::sleep(Duration::from_millis(1));
46                }
47                Err(mpsc::TryRecvError::Disconnected) => break,
48            }
49        }
50
51        messages
52    }
53
54    /// Execute a tick command immediately (for testing)
55    pub fn execute_tick_now<M: Message, F>(&self, _duration: Duration, f: F) -> M
56    where
57        F: FnOnce() -> M,
58    {
59        // In tests, ignore the duration and execute immediately
60        f()
61    }
62
63    /// Execute an async future and wait for result
64    pub fn block_on_async<M: Message + Send + 'static>(
65        &self,
66        future: impl std::future::Future<Output = Option<M>> + Send + 'static,
67    ) -> Option<M> {
68        // Use the shared runtime through CommandExecutor
69        use crate::shared_runtime::shared_runtime;
70        shared_runtime().block_on(future)
71    }
72
73    /// Execute a command and wait for completion
74    pub fn execute_and_wait<M: Message + Clone + Send + 'static>(
75        &self,
76        cmd: Cmd<M>,
77        wait_duration: Duration,
78    ) -> Vec<M> {
79        let (tx, rx) = mpsc::sync_channel(100);
80
81        use crate::program::CommandExecutor;
82        let executor = CommandExecutor::new().expect("Failed to create executor");
83
84        // Start command execution
85        executor.execute(cmd, &tx);
86
87        // Wait for operations to complete
88        std::thread::sleep(wait_duration);
89
90        // Collect all messages
91        let mut messages = Vec::new();
92        while let Ok(Event::User(msg)) = rx.try_recv() {
93            messages.push(msg);
94        }
95
96        messages
97    }
98}
99
100/// Extension trait for Cmd to make testing easier
101pub trait CmdTestExt<M: Message> {
102    /// Execute the command synchronously in tests
103    fn execute_sync(self) -> Option<M>;
104
105    /// Execute with a test harness
106    fn execute_with_harness(self, harness: &AsyncTestHarness) -> Vec<M>;
107}
108
109impl<M: Message + Clone + Send + 'static> CmdTestExt<M> for Cmd<M> {
110    fn execute_sync(self) -> Option<M> {
111        // For simple commands, execute directly
112        if !self.is_tick() && !self.is_every() && !self.is_async() {
113            self.test_execute().ok().flatten()
114        } else {
115            // For async/timed commands, use a harness
116            let harness = AsyncTestHarness::new();
117            harness.execute_command(self).into_iter().next()
118        }
119    }
120
121    fn execute_with_harness(self, harness: &AsyncTestHarness) -> Vec<M> {
122        harness.execute_command(self)
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use hojicha_core::commands;
130
131    #[derive(Debug, Clone, PartialEq)]
132    enum TestMsg {
133        Tick,
134        #[allow(dead_code)]
135        Every,
136        Async,
137    }
138
139    #[test]
140    fn test_async_harness() {
141        let harness = AsyncTestHarness::new();
142
143        // Test tick command
144        let tick_cmd = commands::tick(Duration::from_millis(10), || TestMsg::Tick);
145        let messages = harness.execute_command(tick_cmd);
146        assert_eq!(messages, vec![TestMsg::Tick]);
147    }
148
149    #[test]
150    fn test_execute_tick_now() {
151        let harness = AsyncTestHarness::new();
152        let msg = harness.execute_tick_now(Duration::from_secs(100), || TestMsg::Tick);
153        assert_eq!(msg, TestMsg::Tick);
154    }
155
156    #[test]
157    fn test_block_on_async() {
158        let harness = AsyncTestHarness::new();
159        let result = harness.block_on_async(async {
160            tokio::time::sleep(Duration::from_millis(1)).await;
161            Some(TestMsg::Async)
162        });
163        assert_eq!(result, Some(TestMsg::Async));
164    }
165
166    #[test]
167    fn test_cmd_sync_execution() {
168        // Test that simple commands can be executed synchronously
169        let cmd = commands::custom(|| Some(TestMsg::Async));
170        let result = cmd.execute_sync();
171        assert_eq!(result, Some(TestMsg::Async));
172    }
173}