hojicha_runtime/testing/
async_test_utils.rs1use hojicha_core::core::{Cmd, Message};
4use hojicha_core::event::Event;
5use std::sync::mpsc;
6use std::time::Duration;
7
8pub struct AsyncTestHarness;
10
11impl Default for AsyncTestHarness {
12 fn default() -> Self {
13 Self::new()
14 }
15}
16
17impl AsyncTestHarness {
18 pub fn new() -> Self {
20 Self
21 }
22
23 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 use crate::program::CommandExecutor;
29 let executor = CommandExecutor::new().expect("Failed to create executor");
30
31 executor.execute(cmd, &tx);
33
34 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(_) => {} Err(mpsc::TryRecvError::Empty) => {
44 std::thread::sleep(Duration::from_millis(1));
46 }
47 Err(mpsc::TryRecvError::Disconnected) => break,
48 }
49 }
50
51 messages
52 }
53
54 pub fn execute_tick_now<M: Message, F>(&self, _duration: Duration, f: F) -> M
56 where
57 F: FnOnce() -> M,
58 {
59 f()
61 }
62
63 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 crate::shared_runtime::shared_runtime;
70 shared_runtime().block_on(future)
71 }
72
73 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 executor.execute(cmd, &tx);
86
87 std::thread::sleep(wait_duration);
89
90 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
100pub trait CmdTestExt<M: Message> {
102 fn execute_sync(self) -> Option<M>;
104
105 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 if !self.is_tick() && !self.is_every() && !self.is_async() {
113 self.test_execute().ok().flatten()
114 } else {
115 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 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 let cmd = commands::custom(|| Some(TestMsg::Async));
170 let result = cmd.execute_sync();
171 assert_eq!(result, Some(TestMsg::Async));
172 }
173}