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;
7use tokio::runtime::Runtime;
8
9/// A test harness for async operations with controllable time
10pub struct AsyncTestHarness {
11    runtime: Runtime,
12}
13
14impl AsyncTestHarness {
15    /// Create a new test harness
16    pub fn new() -> Self {
17        Self {
18            runtime: Runtime::new().expect("Failed to create runtime"),
19        }
20    }
21
22    /// Execute a command and collect all messages it produces
23    pub fn execute_command<M: Message + Clone + Send + 'static>(
24        &self,
25        cmd: Cmd<M>,
26    ) -> Vec<M> {
27        let (tx, rx) = mpsc::sync_channel(100);
28        
29        // Import CommandExecutor
30        use crate::program::CommandExecutor;
31        let executor = CommandExecutor::new().expect("Failed to create executor");
32        
33        // Execute the command
34        executor.execute(cmd, tx);
35        
36        // Collect messages with a timeout
37        let mut messages = Vec::new();
38        let start = std::time::Instant::now();
39        let timeout = Duration::from_secs(1);
40        
41        while start.elapsed() < timeout {
42            match rx.try_recv() {
43                Ok(Event::User(msg)) => messages.push(msg),
44                Ok(_) => {} // Ignore other events
45                Err(mpsc::TryRecvError::Empty) => {
46                    // Give async tasks time to complete
47                    std::thread::sleep(Duration::from_millis(1));
48                }
49                Err(mpsc::TryRecvError::Disconnected) => break,
50            }
51        }
52        
53        messages
54    }
55
56    /// Execute a tick command immediately (for testing)
57    pub fn execute_tick_now<M: Message, F>(&self, _duration: Duration, f: F) -> M
58    where
59        F: FnOnce() -> M,
60    {
61        // In tests, ignore the duration and execute immediately
62        f()
63    }
64
65    /// Execute an async future and wait for result
66    pub fn block_on_async<M: Message + Send + 'static>(
67        &self,
68        future: impl std::future::Future<Output = Option<M>> + Send + 'static,
69    ) -> Option<M> {
70        self.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        Every,
135        Async,
136    }
137    
138    #[test]
139    fn test_async_harness() {
140        let harness = AsyncTestHarness::new();
141        
142        // Test tick command
143        let tick_cmd = commands::tick(Duration::from_millis(10), || TestMsg::Tick);
144        let messages = harness.execute_command(tick_cmd);
145        assert_eq!(messages, vec![TestMsg::Tick]);
146    }
147    
148    #[test]
149    fn test_execute_tick_now() {
150        let harness = AsyncTestHarness::new();
151        let msg = harness.execute_tick_now(Duration::from_secs(100), || TestMsg::Tick);
152        assert_eq!(msg, TestMsg::Tick);
153    }
154    
155    #[test]
156    fn test_block_on_async() {
157        let harness = AsyncTestHarness::new();
158        let result = harness.block_on_async(async {
159            tokio::time::sleep(Duration::from_millis(1)).await;
160            Some(TestMsg::Async)
161        });
162        assert_eq!(result, Some(TestMsg::Async));
163    }
164    
165    #[test]
166    fn test_cmd_sync_execution() {
167        // Test that simple commands can be executed synchronously
168        let cmd = commands::custom(|| Some(TestMsg::Async));
169        let result = cmd.execute_sync();
170        assert_eq!(result, Some(TestMsg::Async));
171    }
172}