hojicha_core/testing/
mod.rs1pub mod event_recorder;
6pub mod event_test_harness;
7pub mod test_backend;
8pub mod time_control;
9pub mod unified_harness;
10
11pub use event_recorder::{EventRecorder, RecordedEvent};
12pub use event_test_harness::{EventTestHarness, PriorityEventTestHarness};
13pub use test_backend::TestBackend;
14pub use time_control::*;
15pub use unified_harness::*;
16
17use crate::{
18 core::{Cmd, Model},
19 event::Event,
20};
21use std::sync::{Arc, Mutex};
22
23pub struct TestHarness<M: Model> {
25 model: M,
26 events: Vec<Event<M::Message>>,
27 outputs: Arc<Mutex<Vec<String>>>,
28}
29
30impl<M: Model> TestHarness<M> {
31 pub fn new(model: M) -> Self {
33 Self {
34 model,
35 events: Vec::new(),
36 outputs: Arc::new(Mutex::new(Vec::new())),
37 }
38 }
39
40 pub fn send_event(mut self, event: Event<M::Message>) -> Self {
42 self.events.push(event);
43 self
44 }
45
46 pub fn send_events(mut self, events: Vec<Event<M::Message>>) -> Self {
48 self.events.extend(events);
49 self
50 }
51
52 pub fn run(mut self) -> TestResult<M>
54 where
55 M::Message: std::fmt::Debug,
56 Cmd<M::Message>: std::fmt::Debug,
57 {
58 let outputs = Arc::clone(&self.outputs);
59 let mut commands_executed = Vec::new();
60
61 let cmd = self.model.init();
63 if !cmd.is_noop() {
64 commands_executed.push(format!("Init command: {cmd:?}"));
65 }
66
67 for event in self.events {
69 let event_str = format!("{event:?}");
70 let cmd = self.model.update(event);
71 if !cmd.is_noop() {
72 commands_executed.push(format!("Command from {event_str}: {cmd:?}"));
73 }
74 }
75
76 let final_outputs = outputs.lock().unwrap().clone();
77 TestResult {
78 model: self.model,
79 outputs: final_outputs,
80 commands_executed,
81 }
82 }
83}
84
85pub struct TestResult<M: Model> {
87 pub model: M,
89 pub outputs: Vec<String>,
91 pub commands_executed: Vec<String>,
93}
94
95impl<M: Model> TestResult<M> {
96 pub fn assert_output_contains(&self, expected: &str) -> &Self {
98 assert!(
99 self.outputs.iter().any(|o| o.contains(expected)),
100 "Expected output to contain '{}', but got: {:?}",
101 expected,
102 self.outputs
103 );
104 self
105 }
106
107 pub fn assert_command_executed(&self, command_substr: &str) -> &Self {
109 assert!(
110 self.commands_executed
111 .iter()
112 .any(|c| c.contains(command_substr)),
113 "Expected command containing '{}' to be executed, but got: {:?}",
114 command_substr,
115 self.commands_executed
116 );
117 self
118 }
119
120 pub fn into_model(self) -> M {
122 self.model
123 }
124}
125
126#[macro_export]
128macro_rules! test_events {
129 ($($event:expr),* $(,)?) => {
130 vec![$($event),*]
131 };
132}
133
134#[macro_export]
136macro_rules! key {
137 (Char($ch:expr)) => {
138 Event::Key($crate::event::KeyEvent::new(
139 $crate::event::Key::Char($ch),
140 $crate::event::KeyModifiers::empty(),
141 ))
142 };
143 (Enter) => {
144 Event::Key($crate::event::KeyEvent::new(
145 $crate::event::Key::Enter,
146 $crate::event::KeyModifiers::empty(),
147 ))
148 };
149 (Esc) => {
150 Event::Key($crate::event::KeyEvent::new(
151 $crate::event::Key::Esc,
152 $crate::event::KeyModifiers::empty(),
153 ))
154 };
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 struct TestModel {
162 counter: i32,
163 }
164
165 impl Model for TestModel {
166 type Message = i32;
167
168 fn init(&mut self) -> Cmd<Self::Message> {
169 Cmd::noop()
170 }
171
172 fn update(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
173 if let Some(n) = event.as_user() {
174 self.counter += *n;
175 }
176 Cmd::noop()
177 }
178
179 fn view(&self) -> String {
180 format!("Counter: {}", self.counter)
181 }
182 }
183
184 #[test]
185 fn test_harness_basic() {
186 let model = TestModel { counter: 0 };
187
188 let result = TestHarness::new(model)
189 .send_event(Event::User(5))
190 .send_event(Event::User(3))
191 .run();
192
193 assert_eq!(result.model.counter, 8);
194 }
195}