1#![forbid(unsafe_code)]
2
3use crate::program::{Cmd, Model};
23use crate::state_persistence::StateRegistry;
24use ftui_core::event::Event;
25use ftui_render::buffer::Buffer;
26use ftui_render::frame::Frame;
27use ftui_render::grapheme_pool::GraphemePool;
28use std::sync::Arc;
29use std::time::Duration;
30
31#[derive(Debug, Clone)]
33pub enum CmdRecord {
34 None,
36 Quit,
38 Msg,
40 Batch(usize),
42 Sequence(usize),
44 Tick(Duration),
46 Log(String),
48 Task,
50 MouseCapture(bool),
52}
53
54pub struct ProgramSimulator<M: Model> {
59 model: M,
61 pool: GraphemePool,
63 frames: Vec<Buffer>,
65 command_log: Vec<CmdRecord>,
67 running: bool,
69 tick_rate: Option<Duration>,
71 logs: Vec<String>,
73 state_registry: Option<Arc<StateRegistry>>,
75}
76
77impl<M: Model> ProgramSimulator<M> {
78 pub fn new(model: M) -> Self {
82 Self {
83 model,
84 pool: GraphemePool::new(),
85 frames: Vec::new(),
86 command_log: Vec::new(),
87 running: true,
88 tick_rate: None,
89 logs: Vec::new(),
90 state_registry: None,
91 }
92 }
93
94 pub fn with_registry(model: M, registry: Arc<StateRegistry>) -> Self {
99 let mut sim = Self::new(model);
100 sim.state_registry = Some(registry);
101 sim
102 }
103
104 pub fn init(&mut self) {
108 let cmd = self.model.init();
109 self.execute_cmd(cmd);
110 }
111
112 pub fn inject_events(&mut self, events: &[Event]) {
117 for event in events {
118 if !self.running {
119 break;
120 }
121 let msg = M::Message::from(event.clone());
122 let cmd = self.model.update(msg);
123 self.execute_cmd(cmd);
124 }
125 }
126
127 pub fn inject_event(&mut self, event: Event) {
132 self.inject_events(&[event]);
133 }
134
135 pub fn send(&mut self, msg: M::Message) {
140 if !self.running {
141 return;
142 }
143 let cmd = self.model.update(msg);
144 self.execute_cmd(cmd);
145 }
146
147 pub fn capture_frame(&mut self, width: u16, height: u16) -> &Buffer {
152 let mut frame = Frame::new(width, height, &mut self.pool);
153 self.model.view(&mut frame);
154 self.frames.push(frame.buffer);
155 self.frames.last().expect("frame just pushed")
156 }
157
158 pub fn frames(&self) -> &[Buffer] {
160 &self.frames
161 }
162
163 pub fn last_frame(&self) -> Option<&Buffer> {
165 self.frames.last()
166 }
167
168 pub fn frame_count(&self) -> usize {
170 self.frames.len()
171 }
172
173 #[inline]
175 pub fn model(&self) -> &M {
176 &self.model
177 }
178
179 #[inline]
181 pub fn model_mut(&mut self) -> &mut M {
182 &mut self.model
183 }
184
185 #[inline]
187 pub fn pool(&self) -> &GraphemePool {
188 &self.pool
189 }
190
191 #[inline]
195 pub fn is_running(&self) -> bool {
196 self.running
197 }
198
199 #[inline]
201 pub fn tick_rate(&self) -> Option<Duration> {
202 self.tick_rate
203 }
204
205 #[inline]
207 pub fn logs(&self) -> &[String] {
208 &self.logs
209 }
210
211 #[inline]
213 pub fn command_log(&self) -> &[CmdRecord] {
214 &self.command_log
215 }
216
217 pub fn clear_frames(&mut self) {
219 self.frames.clear();
220 }
221
222 pub fn clear_logs(&mut self) {
224 self.logs.clear();
225 }
226
227 fn execute_cmd(&mut self, cmd: Cmd<M::Message>) {
233 match cmd {
234 Cmd::None => {
235 self.command_log.push(CmdRecord::None);
236 }
237 Cmd::Quit => {
238 self.running = false;
239 self.command_log.push(CmdRecord::Quit);
240 }
241 Cmd::Msg(m) => {
242 self.command_log.push(CmdRecord::Msg);
243 let cmd = self.model.update(m);
244 self.execute_cmd(cmd);
245 }
246 Cmd::Batch(cmds) => {
247 let count = cmds.len();
248 self.command_log.push(CmdRecord::Batch(count));
249 for c in cmds {
250 self.execute_cmd(c);
251 if !self.running {
252 break;
253 }
254 }
255 }
256 Cmd::Sequence(cmds) => {
257 let count = cmds.len();
258 self.command_log.push(CmdRecord::Sequence(count));
259 for c in cmds {
260 self.execute_cmd(c);
261 if !self.running {
262 break;
263 }
264 }
265 }
266 Cmd::Tick(duration) => {
267 self.tick_rate = Some(duration);
268 self.command_log.push(CmdRecord::Tick(duration));
269 }
270 Cmd::Log(text) => {
271 self.command_log.push(CmdRecord::Log(text.clone()));
272 self.logs.push(text);
273 }
274 Cmd::SetMouseCapture(enabled) => {
275 self.command_log.push(CmdRecord::MouseCapture(enabled));
276 }
277 Cmd::Task(_, f) => {
278 self.command_log.push(CmdRecord::Task);
279 let msg = f();
280 let cmd = self.model.update(msg);
281 self.execute_cmd(cmd);
282 }
283 Cmd::SaveState => {
284 if let Some(registry) = &self.state_registry {
285 let _ = registry.flush();
286 }
287 }
288 Cmd::RestoreState => {
289 if let Some(registry) = &self.state_registry {
290 let _ = registry.load();
291 }
292 }
293 Cmd::SetTickStrategy(_) => {
294 }
296 }
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use ftui_core::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
304 use std::cell::RefCell;
305 use std::sync::Arc;
306
307 struct Counter {
310 value: i32,
311 initialized: bool,
312 }
313
314 #[derive(Debug)]
315 enum CounterMsg {
316 Increment,
317 Decrement,
318 Reset,
319 Quit,
320 LogValue,
321 BatchIncrement(usize),
322 }
323
324 impl From<Event> for CounterMsg {
325 fn from(event: Event) -> Self {
326 match event {
327 Event::Key(k) if k.code == KeyCode::Char('+') => CounterMsg::Increment,
328 Event::Key(k) if k.code == KeyCode::Char('-') => CounterMsg::Decrement,
329 Event::Key(k) if k.code == KeyCode::Char('r') => CounterMsg::Reset,
330 Event::Key(k) if k.code == KeyCode::Char('q') => CounterMsg::Quit,
331 _ => CounterMsg::Increment,
332 }
333 }
334 }
335
336 impl Model for Counter {
337 type Message = CounterMsg;
338
339 fn init(&mut self) -> Cmd<Self::Message> {
340 self.initialized = true;
341 Cmd::none()
342 }
343
344 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
345 match msg {
346 CounterMsg::Increment => {
347 self.value += 1;
348 Cmd::none()
349 }
350 CounterMsg::Decrement => {
351 self.value -= 1;
352 Cmd::none()
353 }
354 CounterMsg::Reset => {
355 self.value = 0;
356 Cmd::none()
357 }
358 CounterMsg::Quit => Cmd::quit(),
359 CounterMsg::LogValue => Cmd::log(format!("value={}", self.value)),
360 CounterMsg::BatchIncrement(n) => {
361 let cmds: Vec<_> = (0..n).map(|_| Cmd::msg(CounterMsg::Increment)).collect();
362 Cmd::batch(cmds)
363 }
364 }
365 }
366
367 fn view(&self, frame: &mut Frame) {
368 let text = format!("Count: {}", self.value);
370 for (i, c) in text.chars().enumerate() {
371 if (i as u16) < frame.width() {
372 use ftui_render::cell::Cell;
373 frame.buffer.set_raw(i as u16, 0, Cell::from_char(c));
374 }
375 }
376 }
377 }
378
379 fn key_event(c: char) -> Event {
380 Event::Key(KeyEvent {
381 code: KeyCode::Char(c),
382 modifiers: Modifiers::empty(),
383 kind: KeyEventKind::Press,
384 })
385 }
386
387 fn resize_event(width: u16, height: u16) -> Event {
388 Event::Resize { width, height }
389 }
390
391 #[derive(Default)]
392 struct ResizeTracker {
393 last: Option<(u16, u16)>,
394 history: Vec<(u16, u16)>,
395 }
396
397 #[derive(Debug, Clone, Copy)]
398 enum ResizeMsg {
399 Resize(u16, u16),
400 Quit,
401 Noop,
402 }
403
404 impl From<Event> for ResizeMsg {
405 fn from(event: Event) -> Self {
406 match event {
407 Event::Resize { width, height } => Self::Resize(width, height),
408 Event::Key(k) if k.code == KeyCode::Char('q') => Self::Quit,
409 _ => Self::Noop,
410 }
411 }
412 }
413
414 impl Model for ResizeTracker {
415 type Message = ResizeMsg;
416
417 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
418 match msg {
419 ResizeMsg::Resize(width, height) => {
420 self.last = Some((width, height));
421 self.history.push((width, height));
422 Cmd::none()
423 }
424 ResizeMsg::Quit => Cmd::quit(),
425 ResizeMsg::Noop => Cmd::none(),
426 }
427 }
428
429 fn view(&self, _frame: &mut Frame) {}
430 }
431
432 #[derive(Default)]
433 struct PersistModel;
434
435 #[derive(Debug, Clone, Copy)]
436 enum PersistMsg {
437 Save,
438 Restore,
439 Noop,
440 }
441
442 impl From<Event> for PersistMsg {
443 fn from(event: Event) -> Self {
444 match event {
445 Event::Key(k) if k.code == KeyCode::Char('s') => Self::Save,
446 Event::Key(k) if k.code == KeyCode::Char('r') => Self::Restore,
447 _ => Self::Noop,
448 }
449 }
450 }
451
452 impl Model for PersistModel {
453 type Message = PersistMsg;
454
455 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
456 match msg {
457 PersistMsg::Save => Cmd::save_state(),
458 PersistMsg::Restore => Cmd::restore_state(),
459 PersistMsg::Noop => Cmd::none(),
460 }
461 }
462
463 fn view(&self, _frame: &mut Frame) {}
464 }
465
466 #[test]
469 fn new_simulator() {
470 let sim = ProgramSimulator::new(Counter {
471 value: 0,
472 initialized: false,
473 });
474 assert!(sim.is_running());
475 assert_eq!(sim.model().value, 0);
476 assert!(!sim.model().initialized);
477 assert_eq!(sim.frame_count(), 0);
478 assert!(sim.logs().is_empty());
479 }
480
481 #[test]
482 fn init_calls_model_init() {
483 let mut sim = ProgramSimulator::new(Counter {
484 value: 0,
485 initialized: false,
486 });
487 sim.init();
488 assert!(sim.model().initialized);
489 }
490
491 #[test]
492 fn inject_events_processes_all() {
493 let mut sim = ProgramSimulator::new(Counter {
494 value: 0,
495 initialized: false,
496 });
497 sim.init();
498
499 let events = vec![key_event('+'), key_event('+'), key_event('+')];
500 sim.inject_events(&events);
501
502 assert_eq!(sim.model().value, 3);
503 }
504
505 #[test]
506 fn inject_events_stops_on_quit() {
507 let mut sim = ProgramSimulator::new(Counter {
508 value: 0,
509 initialized: false,
510 });
511 sim.init();
512
513 let events = vec![key_event('+'), key_event('q'), key_event('+')];
515 sim.inject_events(&events);
516
517 assert_eq!(sim.model().value, 1);
518 assert!(!sim.is_running());
519 }
520
521 #[test]
522 fn save_state_flushes_registry() {
523 use crate::state_persistence::StateRegistry;
524
525 let registry = Arc::new(StateRegistry::in_memory());
526 registry.set("viewer", 1, vec![1, 2, 3]);
527 assert!(registry.is_dirty());
528
529 let mut sim = ProgramSimulator::with_registry(PersistModel, Arc::clone(®istry));
530 sim.send(PersistMsg::Save);
531
532 assert!(!registry.is_dirty());
533 let stored = registry.get("viewer").expect("entry present");
534 assert_eq!(stored.version, 1);
535 assert_eq!(stored.data, vec![1, 2, 3]);
536 }
537
538 #[test]
539 fn restore_state_round_trips_cache() {
540 use crate::state_persistence::StateRegistry;
541
542 let registry = Arc::new(StateRegistry::in_memory());
543 registry.set("viewer", 7, vec![9, 8, 7]);
544
545 let mut sim = ProgramSimulator::with_registry(PersistModel, Arc::clone(®istry));
546 sim.send(PersistMsg::Save);
547
548 let removed = registry.remove("viewer");
549 assert!(removed.is_some());
550 assert!(registry.get("viewer").is_none());
551
552 sim.send(PersistMsg::Restore);
553 let restored = registry.get("viewer").expect("restored entry");
554 assert_eq!(restored.version, 7);
555 assert_eq!(restored.data, vec![9, 8, 7]);
556 }
557
558 #[test]
559 fn resize_events_apply_in_order() {
560 let mut sim = ProgramSimulator::new(ResizeTracker::default());
561 sim.init();
562
563 let events = vec![
564 resize_event(80, 24),
565 resize_event(100, 40),
566 resize_event(120, 50),
567 ];
568 sim.inject_events(&events);
569
570 assert_eq!(sim.model().history, vec![(80, 24), (100, 40), (120, 50)]);
571 assert_eq!(sim.model().last, Some((120, 50)));
572 }
573
574 #[test]
575 fn resize_events_after_quit_are_ignored() {
576 let mut sim = ProgramSimulator::new(ResizeTracker::default());
577 sim.init();
578
579 let events = vec![resize_event(80, 24), key_event('q'), resize_event(120, 50)];
580 sim.inject_events(&events);
581
582 assert!(!sim.is_running());
583 assert_eq!(sim.model().history, vec![(80, 24)]);
584 assert_eq!(sim.model().last, Some((80, 24)));
585 }
586
587 #[test]
588 fn send_message_directly() {
589 let mut sim = ProgramSimulator::new(Counter {
590 value: 0,
591 initialized: false,
592 });
593 sim.init();
594
595 sim.send(CounterMsg::Increment);
596 sim.send(CounterMsg::Increment);
597 sim.send(CounterMsg::Decrement);
598
599 assert_eq!(sim.model().value, 1);
600 }
601
602 #[test]
603 fn capture_frame_renders_correctly() {
604 let mut sim = ProgramSimulator::new(Counter {
605 value: 42,
606 initialized: false,
607 });
608 sim.init();
609
610 let buf = sim.capture_frame(80, 24);
611
612 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('C'));
614 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('o'));
615 assert_eq!(buf.get(7, 0).unwrap().content.as_char(), Some('4'));
616 assert_eq!(buf.get(8, 0).unwrap().content.as_char(), Some('2'));
617 }
618
619 #[test]
620 fn multiple_frame_captures() {
621 let mut sim = ProgramSimulator::new(Counter {
622 value: 0,
623 initialized: false,
624 });
625 sim.init();
626
627 sim.capture_frame(80, 24);
628 sim.send(CounterMsg::Increment);
629 sim.capture_frame(80, 24);
630
631 assert_eq!(sim.frame_count(), 2);
632
633 assert_eq!(
635 sim.frames()[0].get(7, 0).unwrap().content.as_char(),
636 Some('0')
637 );
638 assert_eq!(
640 sim.frames()[1].get(7, 0).unwrap().content.as_char(),
641 Some('1')
642 );
643 }
644
645 #[test]
646 fn quit_command_stops_running() {
647 let mut sim = ProgramSimulator::new(Counter {
648 value: 0,
649 initialized: false,
650 });
651 sim.init();
652
653 assert!(sim.is_running());
654 sim.send(CounterMsg::Quit);
655 assert!(!sim.is_running());
656 }
657
658 #[test]
659 fn log_command_records_text() {
660 let mut sim = ProgramSimulator::new(Counter {
661 value: 5,
662 initialized: false,
663 });
664 sim.init();
665
666 sim.send(CounterMsg::LogValue);
667
668 assert_eq!(sim.logs(), &["value=5"]);
669 }
670
671 #[test]
672 fn batch_command_executes_all() {
673 let mut sim = ProgramSimulator::new(Counter {
674 value: 0,
675 initialized: false,
676 });
677 sim.init();
678
679 sim.send(CounterMsg::BatchIncrement(5));
680
681 assert_eq!(sim.model().value, 5);
682 }
683
684 #[test]
685 fn tick_command_sets_rate() {
686 let mut sim = ProgramSimulator::new(Counter {
687 value: 0,
688 initialized: false,
689 });
690
691 assert!(sim.tick_rate().is_none());
692
693 sim.execute_cmd(Cmd::tick(Duration::from_millis(100)));
698
699 assert_eq!(sim.tick_rate(), Some(Duration::from_millis(100)));
700 }
701
702 #[test]
703 fn command_log_records_all() {
704 let mut sim = ProgramSimulator::new(Counter {
705 value: 0,
706 initialized: false,
707 });
708 sim.init();
709
710 sim.send(CounterMsg::Increment);
711 sim.send(CounterMsg::Quit);
712
713 assert!(sim.command_log().len() >= 3);
715 assert!(matches!(sim.command_log().last(), Some(CmdRecord::Quit)));
716 }
717
718 #[test]
719 fn clear_frames() {
720 let mut sim = ProgramSimulator::new(Counter {
721 value: 0,
722 initialized: false,
723 });
724 sim.capture_frame(10, 10);
725 sim.capture_frame(10, 10);
726 assert_eq!(sim.frame_count(), 2);
727
728 sim.clear_frames();
729 assert_eq!(sim.frame_count(), 0);
730 }
731
732 #[test]
733 fn clear_logs() {
734 let mut sim = ProgramSimulator::new(Counter {
735 value: 0,
736 initialized: false,
737 });
738 sim.init();
739 sim.send(CounterMsg::LogValue);
740 assert_eq!(sim.logs().len(), 1);
741
742 sim.clear_logs();
743 assert!(sim.logs().is_empty());
744 }
745
746 #[test]
747 fn model_mut_access() {
748 let mut sim = ProgramSimulator::new(Counter {
749 value: 0,
750 initialized: false,
751 });
752
753 sim.model_mut().value = 100;
754 assert_eq!(sim.model().value, 100);
755 }
756
757 #[test]
758 fn last_frame() {
759 let mut sim = ProgramSimulator::new(Counter {
760 value: 0,
761 initialized: false,
762 });
763
764 assert!(sim.last_frame().is_none());
765
766 sim.capture_frame(10, 10);
767 assert!(sim.last_frame().is_some());
768 }
769
770 #[test]
771 fn send_after_quit_is_ignored() {
772 let mut sim = ProgramSimulator::new(Counter {
773 value: 0,
774 initialized: false,
775 });
776 sim.init();
777
778 sim.send(CounterMsg::Quit);
779 assert!(!sim.is_running());
780
781 sim.send(CounterMsg::Increment);
782 assert_eq!(sim.model().value, 0);
784 }
785
786 #[test]
791 fn identical_inputs_yield_identical_outputs() {
792 fn run_scenario() -> (i32, Vec<u8>) {
793 let mut sim = ProgramSimulator::new(Counter {
794 value: 0,
795 initialized: false,
796 });
797 sim.init();
798
799 sim.send(CounterMsg::Increment);
800 sim.send(CounterMsg::Increment);
801 sim.send(CounterMsg::Decrement);
802 sim.send(CounterMsg::BatchIncrement(3));
803
804 let buf = sim.capture_frame(20, 10);
805 let mut frame_bytes = Vec::new();
806 for y in 0..10 {
807 for x in 0..20 {
808 if let Some(cell) = buf.get(x, y)
809 && let Some(c) = cell.content.as_char()
810 {
811 frame_bytes.push(c as u8);
812 }
813 }
814 }
815 (sim.model().value, frame_bytes)
816 }
817
818 let (value1, frame1) = run_scenario();
819 let (value2, frame2) = run_scenario();
820 let (value3, frame3) = run_scenario();
821
822 assert_eq!(value1, value2);
823 assert_eq!(value2, value3);
824 assert_eq!(value1, 4); assert_eq!(frame1, frame2);
827 assert_eq!(frame2, frame3);
828 }
829
830 #[test]
831 fn command_log_records_in_order() {
832 let mut sim = ProgramSimulator::new(Counter {
833 value: 0,
834 initialized: false,
835 });
836 sim.init();
837
838 sim.send(CounterMsg::Increment);
839 sim.send(CounterMsg::LogValue);
840 sim.send(CounterMsg::Increment);
841 sim.send(CounterMsg::LogValue);
842
843 let log = sim.command_log();
844
845 let log_entries: Vec<_> = log
847 .iter()
848 .filter_map(|r| {
849 if let CmdRecord::Log(s) = r {
850 Some(s.as_str())
851 } else {
852 None
853 }
854 })
855 .collect();
856
857 assert_eq!(log_entries, vec!["value=1", "value=2"]);
858 }
859
860 #[test]
861 fn sequence_command_records_correctly() {
862 struct SeqModel {
864 steps: Vec<i32>,
865 }
866
867 #[derive(Debug)]
868 enum SeqMsg {
869 Step(i32),
870 TriggerSeq,
871 }
872
873 impl From<Event> for SeqMsg {
874 fn from(_: Event) -> Self {
875 SeqMsg::Step(0)
876 }
877 }
878
879 impl Model for SeqModel {
880 type Message = SeqMsg;
881
882 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
883 match msg {
884 SeqMsg::Step(n) => {
885 self.steps.push(n);
886 Cmd::none()
887 }
888 SeqMsg::TriggerSeq => Cmd::sequence(vec![
889 Cmd::msg(SeqMsg::Step(1)),
890 Cmd::msg(SeqMsg::Step(2)),
891 Cmd::msg(SeqMsg::Step(3)),
892 ]),
893 }
894 }
895
896 fn view(&self, _frame: &mut Frame) {}
897 }
898
899 let mut sim = ProgramSimulator::new(SeqModel { steps: vec![] });
900 sim.init();
901 sim.send(SeqMsg::TriggerSeq);
902
903 let has_sequence = sim
905 .command_log()
906 .iter()
907 .any(|r| matches!(r, CmdRecord::Sequence(3)));
908 assert!(has_sequence, "Should record Sequence(3)");
909
910 assert_eq!(sim.model().steps, vec![1, 2, 3]);
912 }
913
914 #[test]
915 fn batch_command_records_correctly() {
916 let mut sim = ProgramSimulator::new(Counter {
917 value: 0,
918 initialized: false,
919 });
920 sim.init();
921
922 sim.send(CounterMsg::BatchIncrement(5));
923
924 let has_batch = sim
926 .command_log()
927 .iter()
928 .any(|r| matches!(r, CmdRecord::Batch(5)));
929 assert!(has_batch, "Should record Batch(5)");
930
931 assert_eq!(sim.model().value, 5);
932 }
933
934 struct OrderingModel {
935 trace: RefCell<Vec<&'static str>>,
936 }
937
938 impl OrderingModel {
939 fn new() -> Self {
940 Self {
941 trace: RefCell::new(Vec::new()),
942 }
943 }
944
945 fn trace(&self) -> Vec<&'static str> {
946 self.trace.borrow().clone()
947 }
948 }
949
950 #[derive(Debug)]
951 enum OrderingMsg {
952 Step(&'static str),
953 StartSequence,
954 StartBatch,
955 }
956
957 impl From<Event> for OrderingMsg {
958 fn from(_: Event) -> Self {
959 OrderingMsg::StartSequence
960 }
961 }
962
963 impl Model for OrderingModel {
964 type Message = OrderingMsg;
965
966 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
967 match msg {
968 OrderingMsg::Step(tag) => {
969 self.trace.borrow_mut().push(tag);
970 Cmd::none()
971 }
972 OrderingMsg::StartSequence => Cmd::sequence(vec![
973 Cmd::msg(OrderingMsg::Step("seq-1")),
974 Cmd::msg(OrderingMsg::Step("seq-2")),
975 Cmd::msg(OrderingMsg::Step("seq-3")),
976 ]),
977 OrderingMsg::StartBatch => Cmd::batch(vec![
978 Cmd::msg(OrderingMsg::Step("batch-1")),
979 Cmd::msg(OrderingMsg::Step("batch-2")),
980 Cmd::msg(OrderingMsg::Step("batch-3")),
981 ]),
982 }
983 }
984
985 fn view(&self, _frame: &mut Frame) {
986 self.trace.borrow_mut().push("view");
987 }
988 }
989
990 #[test]
991 fn sequence_preserves_update_order_before_view() {
992 let mut sim = ProgramSimulator::new(OrderingModel::new());
993 sim.init();
994
995 sim.send(OrderingMsg::StartSequence);
996 sim.capture_frame(1, 1);
997
998 assert_eq!(sim.model().trace(), vec!["seq-1", "seq-2", "seq-3", "view"]);
999 }
1000
1001 #[test]
1002 fn batch_preserves_update_order_before_view() {
1003 let mut sim = ProgramSimulator::new(OrderingModel::new());
1004 sim.init();
1005
1006 sim.send(OrderingMsg::StartBatch);
1007 sim.capture_frame(1, 1);
1008
1009 assert_eq!(
1010 sim.model().trace(),
1011 vec!["batch-1", "batch-2", "batch-3", "view"]
1012 );
1013 }
1014
1015 #[test]
1016 fn frame_dimensions_match_request() {
1017 let mut sim = ProgramSimulator::new(Counter {
1018 value: 42,
1019 initialized: false,
1020 });
1021 sim.init();
1022
1023 let buf = sim.capture_frame(100, 50);
1024 assert_eq!(buf.width(), 100);
1025 assert_eq!(buf.height(), 50);
1026 }
1027
1028 #[test]
1029 fn multiple_frame_captures_are_independent() {
1030 let mut sim = ProgramSimulator::new(Counter {
1031 value: 0,
1032 initialized: false,
1033 });
1034 sim.init();
1035
1036 sim.capture_frame(20, 10);
1038
1039 sim.send(CounterMsg::Increment);
1041 sim.send(CounterMsg::Increment);
1042
1043 sim.capture_frame(20, 10);
1045
1046 let frames = sim.frames();
1047 assert_eq!(frames.len(), 2);
1048
1049 assert_eq!(frames[0].get(7, 0).unwrap().content.as_char(), Some('0'));
1051
1052 assert_eq!(frames[1].get(7, 0).unwrap().content.as_char(), Some('2'));
1054 }
1055
1056 #[test]
1057 fn inject_events_processes_in_order() {
1058 let mut sim = ProgramSimulator::new(Counter {
1059 value: 0,
1060 initialized: false,
1061 });
1062 sim.init();
1063
1064 let events = vec![
1066 key_event('+'),
1067 key_event('+'),
1068 key_event('+'),
1069 key_event('-'),
1070 key_event('+'),
1071 ];
1072
1073 sim.inject_events(&events);
1074
1075 assert_eq!(sim.model().value, 3);
1077 }
1078
1079 #[test]
1080 fn task_command_records_task() {
1081 struct TaskModel {
1082 result: Option<i32>,
1083 }
1084
1085 #[derive(Debug)]
1086 enum TaskMsg {
1087 SetResult(i32),
1088 SpawnTask,
1089 }
1090
1091 impl From<Event> for TaskMsg {
1092 fn from(_: Event) -> Self {
1093 TaskMsg::SetResult(0)
1094 }
1095 }
1096
1097 impl Model for TaskModel {
1098 type Message = TaskMsg;
1099
1100 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1101 match msg {
1102 TaskMsg::SetResult(v) => {
1103 self.result = Some(v);
1104 Cmd::none()
1105 }
1106 TaskMsg::SpawnTask => Cmd::task(|| {
1107 TaskMsg::SetResult(42)
1109 }),
1110 }
1111 }
1112
1113 fn view(&self, _frame: &mut Frame) {}
1114 }
1115
1116 let mut sim = ProgramSimulator::new(TaskModel { result: None });
1117 sim.init();
1118 sim.send(TaskMsg::SpawnTask);
1119
1120 assert_eq!(sim.model().result, Some(42));
1122
1123 let has_task = sim
1125 .command_log()
1126 .iter()
1127 .any(|r| matches!(r, CmdRecord::Task));
1128 assert!(has_task);
1129 }
1130
1131 #[test]
1132 fn tick_rate_is_set() {
1133 let mut sim = ProgramSimulator::new(Counter {
1134 value: 0,
1135 initialized: false,
1136 });
1137
1138 assert!(sim.tick_rate().is_none());
1139
1140 sim.execute_cmd(Cmd::tick(std::time::Duration::from_millis(100)));
1141
1142 assert_eq!(sim.tick_rate(), Some(std::time::Duration::from_millis(100)));
1143 }
1144
1145 #[test]
1146 fn logs_accumulate_across_messages() {
1147 let mut sim = ProgramSimulator::new(Counter {
1148 value: 0,
1149 initialized: false,
1150 });
1151 sim.init();
1152
1153 sim.send(CounterMsg::LogValue);
1154 sim.send(CounterMsg::Increment);
1155 sim.send(CounterMsg::LogValue);
1156 sim.send(CounterMsg::Increment);
1157 sim.send(CounterMsg::LogValue);
1158
1159 assert_eq!(sim.logs().len(), 3);
1160 assert_eq!(sim.logs()[0], "value=0");
1161 assert_eq!(sim.logs()[1], "value=1");
1162 assert_eq!(sim.logs()[2], "value=2");
1163 }
1164
1165 #[test]
1166 fn deterministic_frame_content_across_runs() {
1167 fn capture_frame_content(value: i32) -> Vec<Option<char>> {
1168 let mut sim = ProgramSimulator::new(Counter {
1169 value,
1170 initialized: false,
1171 });
1172 sim.init();
1173
1174 let buf = sim.capture_frame(15, 1);
1175 (0..15)
1176 .map(|x| buf.get(x, 0).and_then(|c| c.content.as_char()))
1177 .collect()
1178 }
1179
1180 let content1 = capture_frame_content(123);
1181 let content2 = capture_frame_content(123);
1182 let content3 = capture_frame_content(123);
1183
1184 assert_eq!(content1, content2);
1185 assert_eq!(content2, content3);
1186
1187 let expected: Vec<Option<char>> = "Count: 123"
1189 .chars()
1190 .map(Some)
1191 .chain(std::iter::repeat_n(None, 5))
1192 .collect();
1193 assert_eq!(content1, expected);
1194 }
1195
1196 #[test]
1197 fn complex_scenario_is_deterministic() {
1198 fn run_complex_scenario() -> (i32, usize, Vec<String>) {
1199 let mut sim = ProgramSimulator::new(Counter {
1200 value: 0,
1201 initialized: false,
1202 });
1203 sim.init();
1204
1205 for _ in 0..10 {
1207 sim.send(CounterMsg::Increment);
1208 }
1209 sim.send(CounterMsg::LogValue);
1210
1211 sim.send(CounterMsg::BatchIncrement(5));
1212 sim.send(CounterMsg::LogValue);
1213
1214 for _ in 0..3 {
1215 sim.send(CounterMsg::Decrement);
1216 }
1217 sim.send(CounterMsg::LogValue);
1218
1219 sim.send(CounterMsg::Reset);
1220 sim.send(CounterMsg::LogValue);
1221
1222 sim.capture_frame(20, 10);
1223
1224 (
1225 sim.model().value,
1226 sim.command_log().len(),
1227 sim.logs().to_vec(),
1228 )
1229 }
1230
1231 let result1 = run_complex_scenario();
1232 let result2 = run_complex_scenario();
1233
1234 assert_eq!(result1.0, result2.0);
1235 assert_eq!(result1.1, result2.1);
1236 assert_eq!(result1.2, result2.2);
1237 }
1238
1239 #[test]
1240 fn model_unchanged_when_not_running() {
1241 let mut sim = ProgramSimulator::new(Counter {
1242 value: 5,
1243 initialized: false,
1244 });
1245 sim.init();
1246
1247 sim.send(CounterMsg::Quit);
1248
1249 let value_before = sim.model().value;
1250 sim.send(CounterMsg::Increment);
1251 sim.send(CounterMsg::BatchIncrement(10));
1252 let value_after = sim.model().value;
1253
1254 assert_eq!(value_before, value_after);
1255 }
1256
1257 #[test]
1258 fn init_produces_consistent_command_log() {
1259 struct InitModel {
1261 init_ran: bool,
1262 }
1263
1264 #[derive(Debug)]
1265 enum InitMsg {
1266 MarkInit,
1267 }
1268
1269 impl From<Event> for InitMsg {
1270 fn from(_: Event) -> Self {
1271 InitMsg::MarkInit
1272 }
1273 }
1274
1275 impl Model for InitModel {
1276 type Message = InitMsg;
1277
1278 fn init(&mut self) -> Cmd<Self::Message> {
1279 Cmd::msg(InitMsg::MarkInit)
1280 }
1281
1282 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1283 match msg {
1284 InitMsg::MarkInit => {
1285 self.init_ran = true;
1286 Cmd::none()
1287 }
1288 }
1289 }
1290
1291 fn view(&self, _frame: &mut Frame) {}
1292 }
1293
1294 let mut sim1 = ProgramSimulator::new(InitModel { init_ran: false });
1295 let mut sim2 = ProgramSimulator::new(InitModel { init_ran: false });
1296
1297 sim1.init();
1298 sim2.init();
1299
1300 assert_eq!(sim1.model().init_ran, sim2.model().init_ran);
1301 assert_eq!(sim1.command_log().len(), sim2.command_log().len());
1302 }
1303
1304 #[test]
1305 fn execute_cmd_directly() {
1306 let mut sim = ProgramSimulator::new(Counter {
1307 value: 0,
1308 initialized: false,
1309 });
1310
1311 sim.execute_cmd(Cmd::log("direct log"));
1313 sim.execute_cmd(Cmd::tick(std::time::Duration::from_secs(1)));
1314
1315 assert_eq!(sim.logs(), &["direct log"]);
1316 assert_eq!(sim.tick_rate(), Some(std::time::Duration::from_secs(1)));
1317 }
1318
1319 #[test]
1320 fn save_restore_are_noops_in_simulator() {
1321 let mut sim = ProgramSimulator::new(Counter {
1322 value: 7,
1323 initialized: false,
1324 });
1325 sim.init();
1326
1327 let log_len = sim.command_log().len();
1328 let tick_rate = sim.tick_rate();
1329 let value_before = sim.model().value;
1330
1331 sim.execute_cmd(Cmd::save_state());
1332 sim.execute_cmd(Cmd::restore_state());
1333
1334 assert_eq!(sim.command_log().len(), log_len);
1335 assert_eq!(sim.tick_rate(), tick_rate);
1336 assert_eq!(sim.model().value, value_before);
1337 assert!(sim.is_running());
1338 }
1339
1340 #[test]
1341 fn grapheme_pool_is_reused() {
1342 let mut sim = ProgramSimulator::new(Counter {
1343 value: 0,
1344 initialized: false,
1345 });
1346 sim.init();
1347
1348 for i in 0..10 {
1350 sim.model_mut().value = i;
1351 sim.capture_frame(80, 24);
1352 }
1353
1354 assert_eq!(sim.frame_count(), 10);
1355 }
1356
1357 #[test]
1368 fn contract_init_called_once_before_updates() {
1369 use std::sync::atomic::{AtomicUsize, Ordering as AO};
1370
1371 struct InitTracker {
1372 init_count: Arc<AtomicUsize>,
1373 update_count: Arc<AtomicUsize>,
1374 init_saw_zero_updates: bool,
1375 }
1376
1377 #[derive(Debug)]
1378 enum TrackerMsg {
1379 FromInit,
1380 Manual,
1381 }
1382
1383 impl From<Event> for TrackerMsg {
1384 fn from(_: Event) -> Self {
1385 TrackerMsg::Manual
1386 }
1387 }
1388
1389 impl Model for InitTracker {
1390 type Message = TrackerMsg;
1391
1392 fn init(&mut self) -> Cmd<Self::Message> {
1393 self.init_count.fetch_add(1, AO::SeqCst);
1394 self.init_saw_zero_updates = self.update_count.load(AO::SeqCst) == 0;
1395 Cmd::msg(TrackerMsg::FromInit)
1396 }
1397
1398 fn update(&mut self, _msg: Self::Message) -> Cmd<Self::Message> {
1399 self.update_count.fetch_add(1, AO::SeqCst);
1400 Cmd::none()
1401 }
1402
1403 fn view(&self, _frame: &mut Frame) {}
1404 }
1405
1406 let init_count = Arc::new(AtomicUsize::new(0));
1407 let update_count = Arc::new(AtomicUsize::new(0));
1408
1409 let mut sim = ProgramSimulator::new(InitTracker {
1410 init_count: init_count.clone(),
1411 update_count: update_count.clone(),
1412 init_saw_zero_updates: false,
1413 });
1414
1415 sim.init();
1416 assert_eq!(init_count.load(AO::SeqCst), 1, "init called exactly once");
1417 assert!(
1418 sim.model().init_saw_zero_updates,
1419 "init must run before any update"
1420 );
1421 assert_eq!(
1423 update_count.load(AO::SeqCst),
1424 1,
1425 "init's command should trigger update"
1426 );
1427 }
1428
1429 #[test]
1432 fn contract_on_shutdown_called_with_final_commands() {
1433 struct ShutdownTracker {
1434 shutdown_called: bool,
1435 final_log: Option<String>,
1436 }
1437
1438 #[derive(Debug)]
1439 enum ShutMsg {
1440 Quit,
1441 LogFinal(String),
1442 }
1443
1444 impl From<Event> for ShutMsg {
1445 fn from(_: Event) -> Self {
1446 ShutMsg::Quit
1447 }
1448 }
1449
1450 impl Model for ShutdownTracker {
1451 type Message = ShutMsg;
1452
1453 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1454 match msg {
1455 ShutMsg::Quit => Cmd::quit(),
1456 ShutMsg::LogFinal(s) => {
1457 self.final_log = Some(s);
1458 Cmd::none()
1459 }
1460 }
1461 }
1462
1463 fn view(&self, _frame: &mut Frame) {}
1464
1465 fn on_shutdown(&mut self) -> Cmd<Self::Message> {
1466 self.shutdown_called = true;
1467 Cmd::msg(ShutMsg::LogFinal("shutdown-complete".into()))
1468 }
1469 }
1470
1471 let mut sim = ProgramSimulator::new(ShutdownTracker {
1472 shutdown_called: false,
1473 final_log: None,
1474 });
1475 sim.init();
1476 sim.send(ShutMsg::Quit);
1477
1478 let shutdown_cmd = sim.model_mut().on_shutdown();
1481 sim.execute_cmd(shutdown_cmd);
1482
1483 assert!(sim.model().shutdown_called, "on_shutdown must be called");
1484 assert_eq!(
1485 sim.model().final_log.as_deref(),
1486 Some("shutdown-complete"),
1487 "on_shutdown commands must be executed"
1488 );
1489 }
1490
1491 #[test]
1493 fn contract_batch_stops_on_quit() {
1494 struct BatchQuitModel {
1495 steps: Vec<&'static str>,
1496 }
1497
1498 #[derive(Debug)]
1499 enum BQMsg {
1500 Step(&'static str),
1501 TriggerBatchWithQuit,
1502 }
1503
1504 impl From<Event> for BQMsg {
1505 fn from(_: Event) -> Self {
1506 BQMsg::Step("event")
1507 }
1508 }
1509
1510 impl Model for BatchQuitModel {
1511 type Message = BQMsg;
1512
1513 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1514 match msg {
1515 BQMsg::Step(s) => {
1516 self.steps.push(s);
1517 Cmd::none()
1518 }
1519 BQMsg::TriggerBatchWithQuit => Cmd::batch(vec![
1520 Cmd::msg(BQMsg::Step("before-quit")),
1521 Cmd::quit(),
1522 Cmd::msg(BQMsg::Step("after-quit")),
1523 ]),
1524 }
1525 }
1526
1527 fn view(&self, _frame: &mut Frame) {}
1528 }
1529
1530 let mut sim = ProgramSimulator::new(BatchQuitModel { steps: vec![] });
1531 sim.init();
1532 sim.send(BQMsg::TriggerBatchWithQuit);
1533
1534 assert!(!sim.is_running(), "should be stopped");
1535 assert_eq!(
1536 sim.model().steps,
1537 vec!["before-quit"],
1538 "commands after Quit in a Batch must not execute"
1539 );
1540 }
1541
1542 #[test]
1544 fn contract_sequence_stops_on_quit() {
1545 struct SeqQuitModel {
1546 steps: Vec<&'static str>,
1547 }
1548
1549 #[derive(Debug)]
1550 enum SQMsg {
1551 Step(&'static str),
1552 TriggerSeqWithQuit,
1553 }
1554
1555 impl From<Event> for SQMsg {
1556 fn from(_: Event) -> Self {
1557 SQMsg::Step("event")
1558 }
1559 }
1560
1561 impl Model for SeqQuitModel {
1562 type Message = SQMsg;
1563
1564 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1565 match msg {
1566 SQMsg::Step(s) => {
1567 self.steps.push(s);
1568 Cmd::none()
1569 }
1570 SQMsg::TriggerSeqWithQuit => Cmd::sequence(vec![
1571 Cmd::msg(SQMsg::Step("before-quit")),
1572 Cmd::quit(),
1573 Cmd::msg(SQMsg::Step("after-quit")),
1574 ]),
1575 }
1576 }
1577
1578 fn view(&self, _frame: &mut Frame) {}
1579 }
1580
1581 let mut sim = ProgramSimulator::new(SeqQuitModel { steps: vec![] });
1582 sim.init();
1583 sim.send(SQMsg::TriggerSeqWithQuit);
1584
1585 assert!(!sim.is_running(), "should be stopped");
1586 assert_eq!(
1587 sim.model().steps,
1588 vec!["before-quit"],
1589 "commands after Quit in a Sequence must not execute"
1590 );
1591 }
1592
1593 #[test]
1596 fn contract_task_result_routes_through_update() {
1597 struct TaskModel {
1598 trace: Vec<String>,
1599 }
1600
1601 #[derive(Debug)]
1602 enum TMsg {
1603 Spawn,
1604 TaskDone(i32),
1605 }
1606
1607 impl From<Event> for TMsg {
1608 fn from(_: Event) -> Self {
1609 TMsg::Spawn
1610 }
1611 }
1612
1613 impl Model for TaskModel {
1614 type Message = TMsg;
1615
1616 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1617 match msg {
1618 TMsg::Spawn => {
1619 self.trace.push("spawn".into());
1620 Cmd::task(|| TMsg::TaskDone(42))
1621 }
1622 TMsg::TaskDone(v) => {
1623 self.trace.push(format!("done:{v}"));
1624 Cmd::none()
1625 }
1626 }
1627 }
1628
1629 fn view(&self, _frame: &mut Frame) {}
1630 }
1631
1632 let mut sim = ProgramSimulator::new(TaskModel { trace: vec![] });
1633 sim.init();
1634 sim.send(TMsg::Spawn);
1635
1636 assert_eq!(
1637 sim.model().trace,
1638 vec!["spawn", "done:42"],
1639 "task result must route through update()"
1640 );
1641 }
1642
1643 #[test]
1646 fn contract_cmd_msg_dispatches_recursively() {
1647 struct RecursiveModel {
1648 trace: Vec<i32>,
1649 }
1650
1651 #[derive(Debug)]
1652 enum RMsg {
1653 Chain(i32),
1654 }
1655
1656 impl From<Event> for RMsg {
1657 fn from(_: Event) -> Self {
1658 RMsg::Chain(0)
1659 }
1660 }
1661
1662 impl Model for RecursiveModel {
1663 type Message = RMsg;
1664
1665 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1666 match msg {
1667 RMsg::Chain(n) => {
1668 self.trace.push(n);
1669 if n < 3 {
1670 Cmd::msg(RMsg::Chain(n + 1))
1671 } else {
1672 Cmd::none()
1673 }
1674 }
1675 }
1676 }
1677
1678 fn view(&self, _frame: &mut Frame) {}
1679 }
1680
1681 let mut sim = ProgramSimulator::new(RecursiveModel { trace: vec![] });
1682 sim.init();
1683 sim.send(RMsg::Chain(0));
1684
1685 assert_eq!(
1686 sim.model().trace,
1687 vec![0, 1, 2, 3],
1688 "Cmd::Msg must dispatch recursively through update()"
1689 );
1690 }
1691
1692 #[test]
1695 fn contract_batch_normalization() {
1696 let empty: Cmd<CounterMsg> = Cmd::batch(vec![]);
1697 assert!(matches!(empty, Cmd::None), "empty batch must be Cmd::None");
1698
1699 let single: Cmd<CounterMsg> = Cmd::batch(vec![Cmd::quit()]);
1700 assert!(
1701 matches!(single, Cmd::Quit),
1702 "single-element batch must unwrap"
1703 );
1704
1705 let multi: Cmd<CounterMsg> = Cmd::batch(vec![Cmd::none(), Cmd::quit()]);
1706 assert!(
1707 matches!(multi, Cmd::Batch(_)),
1708 "multi-element batch stays Batch"
1709 );
1710 }
1711
1712 #[test]
1714 fn contract_sequence_normalization() {
1715 let empty: Cmd<CounterMsg> = Cmd::sequence(vec![]);
1716 assert!(
1717 matches!(empty, Cmd::None),
1718 "empty sequence must be Cmd::None"
1719 );
1720 }
1721
1722 #[test]
1725 fn contract_no_processing_after_quit() {
1726 let mut sim = ProgramSimulator::new(Counter {
1727 value: 0,
1728 initialized: false,
1729 });
1730 sim.init();
1731
1732 sim.send(CounterMsg::Increment); sim.send(CounterMsg::Quit);
1734 sim.send(CounterMsg::Increment); sim.send(CounterMsg::Increment); assert_eq!(sim.model().value, 1, "messages after Quit must be ignored");
1738 assert!(!sim.is_running());
1739
1740 sim.inject_events(&[key_event('+'), key_event('+')]);
1742 assert_eq!(
1743 sim.model().value,
1744 1,
1745 "events after Quit must also be ignored"
1746 );
1747 }
1748}