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}
51
52pub struct ProgramSimulator<M: Model> {
57 model: M,
59 pool: GraphemePool,
61 frames: Vec<Buffer>,
63 command_log: Vec<CmdRecord>,
65 running: bool,
67 tick_rate: Option<Duration>,
69 logs: Vec<String>,
71 state_registry: Option<Arc<StateRegistry>>,
73}
74
75impl<M: Model> ProgramSimulator<M> {
76 pub fn new(model: M) -> Self {
80 Self {
81 model,
82 pool: GraphemePool::new(),
83 frames: Vec::new(),
84 command_log: Vec::new(),
85 running: true,
86 tick_rate: None,
87 logs: Vec::new(),
88 state_registry: None,
89 }
90 }
91
92 pub fn with_registry(model: M, registry: Arc<StateRegistry>) -> Self {
97 let mut sim = Self::new(model);
98 sim.state_registry = Some(registry);
99 sim
100 }
101
102 pub fn init(&mut self) {
106 let cmd = self.model.init();
107 self.execute_cmd(cmd);
108 }
109
110 pub fn inject_events(&mut self, events: &[Event]) {
115 for event in events {
116 if !self.running {
117 break;
118 }
119 let msg = M::Message::from(event.clone());
120 let cmd = self.model.update(msg);
121 self.execute_cmd(cmd);
122 }
123 }
124
125 pub fn inject_event(&mut self, event: Event) {
130 self.inject_events(&[event]);
131 }
132
133 pub fn send(&mut self, msg: M::Message) {
138 if !self.running {
139 return;
140 }
141 let cmd = self.model.update(msg);
142 self.execute_cmd(cmd);
143 }
144
145 pub fn capture_frame(&mut self, width: u16, height: u16) -> &Buffer {
150 let mut frame = Frame::new(width, height, &mut self.pool);
151 self.model.view(&mut frame);
152 self.frames.push(frame.buffer);
153 self.frames.last().unwrap()
154 }
155
156 pub fn frames(&self) -> &[Buffer] {
158 &self.frames
159 }
160
161 pub fn last_frame(&self) -> Option<&Buffer> {
163 self.frames.last()
164 }
165
166 pub fn frame_count(&self) -> usize {
168 self.frames.len()
169 }
170
171 pub fn model(&self) -> &M {
173 &self.model
174 }
175
176 pub fn model_mut(&mut self) -> &mut M {
178 &mut self.model
179 }
180
181 pub fn is_running(&self) -> bool {
185 self.running
186 }
187
188 pub fn tick_rate(&self) -> Option<Duration> {
190 self.tick_rate
191 }
192
193 pub fn logs(&self) -> &[String] {
195 &self.logs
196 }
197
198 pub fn command_log(&self) -> &[CmdRecord] {
200 &self.command_log
201 }
202
203 pub fn clear_frames(&mut self) {
205 self.frames.clear();
206 }
207
208 pub fn clear_logs(&mut self) {
210 self.logs.clear();
211 }
212
213 fn execute_cmd(&mut self, cmd: Cmd<M::Message>) {
219 match cmd {
220 Cmd::None => {
221 self.command_log.push(CmdRecord::None);
222 }
223 Cmd::Quit => {
224 self.running = false;
225 self.command_log.push(CmdRecord::Quit);
226 }
227 Cmd::Msg(m) => {
228 self.command_log.push(CmdRecord::Msg);
229 let cmd = self.model.update(m);
230 self.execute_cmd(cmd);
231 }
232 Cmd::Batch(cmds) => {
233 let count = cmds.len();
234 self.command_log.push(CmdRecord::Batch(count));
235 for c in cmds {
236 self.execute_cmd(c);
237 if !self.running {
238 break;
239 }
240 }
241 }
242 Cmd::Sequence(cmds) => {
243 let count = cmds.len();
244 self.command_log.push(CmdRecord::Sequence(count));
245 for c in cmds {
246 self.execute_cmd(c);
247 if !self.running {
248 break;
249 }
250 }
251 }
252 Cmd::Tick(duration) => {
253 self.tick_rate = Some(duration);
254 self.command_log.push(CmdRecord::Tick(duration));
255 }
256 Cmd::Log(text) => {
257 self.command_log.push(CmdRecord::Log(text.clone()));
258 self.logs.push(text);
259 }
260 Cmd::Task(_, f) => {
261 self.command_log.push(CmdRecord::Task);
262 let msg = f();
263 let cmd = self.model.update(msg);
264 self.execute_cmd(cmd);
265 }
266 Cmd::SaveState => {
267 if let Some(registry) = &self.state_registry {
268 let _ = registry.flush();
269 }
270 }
271 Cmd::RestoreState => {
272 if let Some(registry) = &self.state_registry {
273 let _ = registry.load();
274 }
275 }
276 }
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use ftui_core::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
284 use std::cell::RefCell;
285 use std::sync::Arc;
286
287 struct Counter {
290 value: i32,
291 initialized: bool,
292 }
293
294 #[derive(Debug)]
295 enum CounterMsg {
296 Increment,
297 Decrement,
298 Reset,
299 Quit,
300 LogValue,
301 BatchIncrement(usize),
302 }
303
304 impl From<Event> for CounterMsg {
305 fn from(event: Event) -> Self {
306 match event {
307 Event::Key(k) if k.code == KeyCode::Char('+') => CounterMsg::Increment,
308 Event::Key(k) if k.code == KeyCode::Char('-') => CounterMsg::Decrement,
309 Event::Key(k) if k.code == KeyCode::Char('r') => CounterMsg::Reset,
310 Event::Key(k) if k.code == KeyCode::Char('q') => CounterMsg::Quit,
311 _ => CounterMsg::Increment,
312 }
313 }
314 }
315
316 impl Model for Counter {
317 type Message = CounterMsg;
318
319 fn init(&mut self) -> Cmd<Self::Message> {
320 self.initialized = true;
321 Cmd::none()
322 }
323
324 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
325 match msg {
326 CounterMsg::Increment => {
327 self.value += 1;
328 Cmd::none()
329 }
330 CounterMsg::Decrement => {
331 self.value -= 1;
332 Cmd::none()
333 }
334 CounterMsg::Reset => {
335 self.value = 0;
336 Cmd::none()
337 }
338 CounterMsg::Quit => Cmd::quit(),
339 CounterMsg::LogValue => Cmd::log(format!("value={}", self.value)),
340 CounterMsg::BatchIncrement(n) => {
341 let cmds: Vec<_> = (0..n).map(|_| Cmd::msg(CounterMsg::Increment)).collect();
342 Cmd::batch(cmds)
343 }
344 }
345 }
346
347 fn view(&self, frame: &mut Frame) {
348 let text = format!("Count: {}", self.value);
350 for (i, c) in text.chars().enumerate() {
351 if (i as u16) < frame.width() {
352 use ftui_render::cell::Cell;
353 frame.buffer.set_raw(i as u16, 0, Cell::from_char(c));
354 }
355 }
356 }
357 }
358
359 fn key_event(c: char) -> Event {
360 Event::Key(KeyEvent {
361 code: KeyCode::Char(c),
362 modifiers: Modifiers::empty(),
363 kind: KeyEventKind::Press,
364 })
365 }
366
367 fn resize_event(width: u16, height: u16) -> Event {
368 Event::Resize { width, height }
369 }
370
371 #[derive(Default)]
372 struct ResizeTracker {
373 last: Option<(u16, u16)>,
374 history: Vec<(u16, u16)>,
375 }
376
377 #[derive(Debug, Clone, Copy)]
378 enum ResizeMsg {
379 Resize(u16, u16),
380 Quit,
381 Noop,
382 }
383
384 impl From<Event> for ResizeMsg {
385 fn from(event: Event) -> Self {
386 match event {
387 Event::Resize { width, height } => Self::Resize(width, height),
388 Event::Key(k) if k.code == KeyCode::Char('q') => Self::Quit,
389 _ => Self::Noop,
390 }
391 }
392 }
393
394 impl Model for ResizeTracker {
395 type Message = ResizeMsg;
396
397 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
398 match msg {
399 ResizeMsg::Resize(width, height) => {
400 self.last = Some((width, height));
401 self.history.push((width, height));
402 Cmd::none()
403 }
404 ResizeMsg::Quit => Cmd::quit(),
405 ResizeMsg::Noop => Cmd::none(),
406 }
407 }
408
409 fn view(&self, _frame: &mut Frame) {}
410 }
411
412 #[derive(Default)]
413 struct PersistModel;
414
415 #[derive(Debug, Clone, Copy)]
416 enum PersistMsg {
417 Save,
418 Restore,
419 Noop,
420 }
421
422 impl From<Event> for PersistMsg {
423 fn from(event: Event) -> Self {
424 match event {
425 Event::Key(k) if k.code == KeyCode::Char('s') => Self::Save,
426 Event::Key(k) if k.code == KeyCode::Char('r') => Self::Restore,
427 _ => Self::Noop,
428 }
429 }
430 }
431
432 impl Model for PersistModel {
433 type Message = PersistMsg;
434
435 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
436 match msg {
437 PersistMsg::Save => Cmd::save_state(),
438 PersistMsg::Restore => Cmd::restore_state(),
439 PersistMsg::Noop => Cmd::none(),
440 }
441 }
442
443 fn view(&self, _frame: &mut Frame) {}
444 }
445
446 #[test]
449 fn new_simulator() {
450 let sim = ProgramSimulator::new(Counter {
451 value: 0,
452 initialized: false,
453 });
454 assert!(sim.is_running());
455 assert_eq!(sim.model().value, 0);
456 assert!(!sim.model().initialized);
457 assert_eq!(sim.frame_count(), 0);
458 assert!(sim.logs().is_empty());
459 }
460
461 #[test]
462 fn init_calls_model_init() {
463 let mut sim = ProgramSimulator::new(Counter {
464 value: 0,
465 initialized: false,
466 });
467 sim.init();
468 assert!(sim.model().initialized);
469 }
470
471 #[test]
472 fn inject_events_processes_all() {
473 let mut sim = ProgramSimulator::new(Counter {
474 value: 0,
475 initialized: false,
476 });
477 sim.init();
478
479 let events = vec![key_event('+'), key_event('+'), key_event('+')];
480 sim.inject_events(&events);
481
482 assert_eq!(sim.model().value, 3);
483 }
484
485 #[test]
486 fn inject_events_stops_on_quit() {
487 let mut sim = ProgramSimulator::new(Counter {
488 value: 0,
489 initialized: false,
490 });
491 sim.init();
492
493 let events = vec![key_event('+'), key_event('q'), key_event('+')];
495 sim.inject_events(&events);
496
497 assert_eq!(sim.model().value, 1);
498 assert!(!sim.is_running());
499 }
500
501 #[test]
502 fn save_state_flushes_registry() {
503 use crate::state_persistence::StateRegistry;
504
505 let registry = Arc::new(StateRegistry::in_memory());
506 registry.set("viewer", 1, vec![1, 2, 3]);
507 assert!(registry.is_dirty());
508
509 let mut sim = ProgramSimulator::with_registry(PersistModel, Arc::clone(®istry));
510 sim.send(PersistMsg::Save);
511
512 assert!(!registry.is_dirty());
513 let stored = registry.get("viewer").expect("entry present");
514 assert_eq!(stored.version, 1);
515 assert_eq!(stored.data, vec![1, 2, 3]);
516 }
517
518 #[test]
519 fn restore_state_round_trips_cache() {
520 use crate::state_persistence::StateRegistry;
521
522 let registry = Arc::new(StateRegistry::in_memory());
523 registry.set("viewer", 7, vec![9, 8, 7]);
524
525 let mut sim = ProgramSimulator::with_registry(PersistModel, Arc::clone(®istry));
526 sim.send(PersistMsg::Save);
527
528 let removed = registry.remove("viewer");
529 assert!(removed.is_some());
530 assert!(registry.get("viewer").is_none());
531
532 sim.send(PersistMsg::Restore);
533 let restored = registry.get("viewer").expect("restored entry");
534 assert_eq!(restored.version, 7);
535 assert_eq!(restored.data, vec![9, 8, 7]);
536 }
537
538 #[test]
539 fn resize_events_apply_in_order() {
540 let mut sim = ProgramSimulator::new(ResizeTracker::default());
541 sim.init();
542
543 let events = vec![
544 resize_event(80, 24),
545 resize_event(100, 40),
546 resize_event(120, 50),
547 ];
548 sim.inject_events(&events);
549
550 assert_eq!(sim.model().history, vec![(80, 24), (100, 40), (120, 50)]);
551 assert_eq!(sim.model().last, Some((120, 50)));
552 }
553
554 #[test]
555 fn resize_events_after_quit_are_ignored() {
556 let mut sim = ProgramSimulator::new(ResizeTracker::default());
557 sim.init();
558
559 let events = vec![resize_event(80, 24), key_event('q'), resize_event(120, 50)];
560 sim.inject_events(&events);
561
562 assert!(!sim.is_running());
563 assert_eq!(sim.model().history, vec![(80, 24)]);
564 assert_eq!(sim.model().last, Some((80, 24)));
565 }
566
567 #[test]
568 fn send_message_directly() {
569 let mut sim = ProgramSimulator::new(Counter {
570 value: 0,
571 initialized: false,
572 });
573 sim.init();
574
575 sim.send(CounterMsg::Increment);
576 sim.send(CounterMsg::Increment);
577 sim.send(CounterMsg::Decrement);
578
579 assert_eq!(sim.model().value, 1);
580 }
581
582 #[test]
583 fn capture_frame_renders_correctly() {
584 let mut sim = ProgramSimulator::new(Counter {
585 value: 42,
586 initialized: false,
587 });
588 sim.init();
589
590 let buf = sim.capture_frame(80, 24);
591
592 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('C'));
594 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('o'));
595 assert_eq!(buf.get(7, 0).unwrap().content.as_char(), Some('4'));
596 assert_eq!(buf.get(8, 0).unwrap().content.as_char(), Some('2'));
597 }
598
599 #[test]
600 fn multiple_frame_captures() {
601 let mut sim = ProgramSimulator::new(Counter {
602 value: 0,
603 initialized: false,
604 });
605 sim.init();
606
607 sim.capture_frame(80, 24);
608 sim.send(CounterMsg::Increment);
609 sim.capture_frame(80, 24);
610
611 assert_eq!(sim.frame_count(), 2);
612
613 assert_eq!(
615 sim.frames()[0].get(7, 0).unwrap().content.as_char(),
616 Some('0')
617 );
618 assert_eq!(
620 sim.frames()[1].get(7, 0).unwrap().content.as_char(),
621 Some('1')
622 );
623 }
624
625 #[test]
626 fn quit_command_stops_running() {
627 let mut sim = ProgramSimulator::new(Counter {
628 value: 0,
629 initialized: false,
630 });
631 sim.init();
632
633 assert!(sim.is_running());
634 sim.send(CounterMsg::Quit);
635 assert!(!sim.is_running());
636 }
637
638 #[test]
639 fn log_command_records_text() {
640 let mut sim = ProgramSimulator::new(Counter {
641 value: 5,
642 initialized: false,
643 });
644 sim.init();
645
646 sim.send(CounterMsg::LogValue);
647
648 assert_eq!(sim.logs(), &["value=5"]);
649 }
650
651 #[test]
652 fn batch_command_executes_all() {
653 let mut sim = ProgramSimulator::new(Counter {
654 value: 0,
655 initialized: false,
656 });
657 sim.init();
658
659 sim.send(CounterMsg::BatchIncrement(5));
660
661 assert_eq!(sim.model().value, 5);
662 }
663
664 #[test]
665 fn tick_command_sets_rate() {
666 let mut sim = ProgramSimulator::new(Counter {
667 value: 0,
668 initialized: false,
669 });
670
671 assert!(sim.tick_rate().is_none());
672
673 sim.execute_cmd(Cmd::tick(Duration::from_millis(100)));
678
679 assert_eq!(sim.tick_rate(), Some(Duration::from_millis(100)));
680 }
681
682 #[test]
683 fn command_log_records_all() {
684 let mut sim = ProgramSimulator::new(Counter {
685 value: 0,
686 initialized: false,
687 });
688 sim.init();
689
690 sim.send(CounterMsg::Increment);
691 sim.send(CounterMsg::Quit);
692
693 assert!(sim.command_log().len() >= 3);
695 assert!(matches!(sim.command_log().last(), Some(CmdRecord::Quit)));
696 }
697
698 #[test]
699 fn clear_frames() {
700 let mut sim = ProgramSimulator::new(Counter {
701 value: 0,
702 initialized: false,
703 });
704 sim.capture_frame(10, 10);
705 sim.capture_frame(10, 10);
706 assert_eq!(sim.frame_count(), 2);
707
708 sim.clear_frames();
709 assert_eq!(sim.frame_count(), 0);
710 }
711
712 #[test]
713 fn clear_logs() {
714 let mut sim = ProgramSimulator::new(Counter {
715 value: 0,
716 initialized: false,
717 });
718 sim.init();
719 sim.send(CounterMsg::LogValue);
720 assert_eq!(sim.logs().len(), 1);
721
722 sim.clear_logs();
723 assert!(sim.logs().is_empty());
724 }
725
726 #[test]
727 fn model_mut_access() {
728 let mut sim = ProgramSimulator::new(Counter {
729 value: 0,
730 initialized: false,
731 });
732
733 sim.model_mut().value = 100;
734 assert_eq!(sim.model().value, 100);
735 }
736
737 #[test]
738 fn last_frame() {
739 let mut sim = ProgramSimulator::new(Counter {
740 value: 0,
741 initialized: false,
742 });
743
744 assert!(sim.last_frame().is_none());
745
746 sim.capture_frame(10, 10);
747 assert!(sim.last_frame().is_some());
748 }
749
750 #[test]
751 fn send_after_quit_is_ignored() {
752 let mut sim = ProgramSimulator::new(Counter {
753 value: 0,
754 initialized: false,
755 });
756 sim.init();
757
758 sim.send(CounterMsg::Quit);
759 assert!(!sim.is_running());
760
761 sim.send(CounterMsg::Increment);
762 assert_eq!(sim.model().value, 0);
764 }
765
766 #[test]
771 fn identical_inputs_yield_identical_outputs() {
772 fn run_scenario() -> (i32, Vec<u8>) {
773 let mut sim = ProgramSimulator::new(Counter {
774 value: 0,
775 initialized: false,
776 });
777 sim.init();
778
779 sim.send(CounterMsg::Increment);
780 sim.send(CounterMsg::Increment);
781 sim.send(CounterMsg::Decrement);
782 sim.send(CounterMsg::BatchIncrement(3));
783
784 let buf = sim.capture_frame(20, 10);
785 let mut frame_bytes = Vec::new();
786 for y in 0..10 {
787 for x in 0..20 {
788 if let Some(cell) = buf.get(x, y)
789 && let Some(c) = cell.content.as_char()
790 {
791 frame_bytes.push(c as u8);
792 }
793 }
794 }
795 (sim.model().value, frame_bytes)
796 }
797
798 let (value1, frame1) = run_scenario();
799 let (value2, frame2) = run_scenario();
800 let (value3, frame3) = run_scenario();
801
802 assert_eq!(value1, value2);
803 assert_eq!(value2, value3);
804 assert_eq!(value1, 4); assert_eq!(frame1, frame2);
807 assert_eq!(frame2, frame3);
808 }
809
810 #[test]
811 fn command_log_records_in_order() {
812 let mut sim = ProgramSimulator::new(Counter {
813 value: 0,
814 initialized: false,
815 });
816 sim.init();
817
818 sim.send(CounterMsg::Increment);
819 sim.send(CounterMsg::LogValue);
820 sim.send(CounterMsg::Increment);
821 sim.send(CounterMsg::LogValue);
822
823 let log = sim.command_log();
824
825 let log_entries: Vec<_> = log
827 .iter()
828 .filter_map(|r| {
829 if let CmdRecord::Log(s) = r {
830 Some(s.as_str())
831 } else {
832 None
833 }
834 })
835 .collect();
836
837 assert_eq!(log_entries, vec!["value=1", "value=2"]);
838 }
839
840 #[test]
841 fn sequence_command_records_correctly() {
842 struct SeqModel {
844 steps: Vec<i32>,
845 }
846
847 #[derive(Debug)]
848 enum SeqMsg {
849 Step(i32),
850 TriggerSeq,
851 }
852
853 impl From<Event> for SeqMsg {
854 fn from(_: Event) -> Self {
855 SeqMsg::Step(0)
856 }
857 }
858
859 impl Model for SeqModel {
860 type Message = SeqMsg;
861
862 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
863 match msg {
864 SeqMsg::Step(n) => {
865 self.steps.push(n);
866 Cmd::none()
867 }
868 SeqMsg::TriggerSeq => Cmd::sequence(vec![
869 Cmd::msg(SeqMsg::Step(1)),
870 Cmd::msg(SeqMsg::Step(2)),
871 Cmd::msg(SeqMsg::Step(3)),
872 ]),
873 }
874 }
875
876 fn view(&self, _frame: &mut Frame) {}
877 }
878
879 let mut sim = ProgramSimulator::new(SeqModel { steps: vec![] });
880 sim.init();
881 sim.send(SeqMsg::TriggerSeq);
882
883 let has_sequence = sim
885 .command_log()
886 .iter()
887 .any(|r| matches!(r, CmdRecord::Sequence(3)));
888 assert!(has_sequence, "Should record Sequence(3)");
889
890 assert_eq!(sim.model().steps, vec![1, 2, 3]);
892 }
893
894 #[test]
895 fn batch_command_records_correctly() {
896 let mut sim = ProgramSimulator::new(Counter {
897 value: 0,
898 initialized: false,
899 });
900 sim.init();
901
902 sim.send(CounterMsg::BatchIncrement(5));
903
904 let has_batch = sim
906 .command_log()
907 .iter()
908 .any(|r| matches!(r, CmdRecord::Batch(5)));
909 assert!(has_batch, "Should record Batch(5)");
910
911 assert_eq!(sim.model().value, 5);
912 }
913
914 struct OrderingModel {
915 trace: RefCell<Vec<&'static str>>,
916 }
917
918 impl OrderingModel {
919 fn new() -> Self {
920 Self {
921 trace: RefCell::new(Vec::new()),
922 }
923 }
924
925 fn trace(&self) -> Vec<&'static str> {
926 self.trace.borrow().clone()
927 }
928 }
929
930 #[derive(Debug)]
931 enum OrderingMsg {
932 Step(&'static str),
933 StartSequence,
934 StartBatch,
935 }
936
937 impl From<Event> for OrderingMsg {
938 fn from(_: Event) -> Self {
939 OrderingMsg::StartSequence
940 }
941 }
942
943 impl Model for OrderingModel {
944 type Message = OrderingMsg;
945
946 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
947 match msg {
948 OrderingMsg::Step(tag) => {
949 self.trace.borrow_mut().push(tag);
950 Cmd::none()
951 }
952 OrderingMsg::StartSequence => Cmd::sequence(vec![
953 Cmd::msg(OrderingMsg::Step("seq-1")),
954 Cmd::msg(OrderingMsg::Step("seq-2")),
955 Cmd::msg(OrderingMsg::Step("seq-3")),
956 ]),
957 OrderingMsg::StartBatch => Cmd::batch(vec![
958 Cmd::msg(OrderingMsg::Step("batch-1")),
959 Cmd::msg(OrderingMsg::Step("batch-2")),
960 Cmd::msg(OrderingMsg::Step("batch-3")),
961 ]),
962 }
963 }
964
965 fn view(&self, _frame: &mut Frame) {
966 self.trace.borrow_mut().push("view");
967 }
968 }
969
970 #[test]
971 fn sequence_preserves_update_order_before_view() {
972 let mut sim = ProgramSimulator::new(OrderingModel::new());
973 sim.init();
974
975 sim.send(OrderingMsg::StartSequence);
976 sim.capture_frame(1, 1);
977
978 assert_eq!(sim.model().trace(), vec!["seq-1", "seq-2", "seq-3", "view"]);
979 }
980
981 #[test]
982 fn batch_preserves_update_order_before_view() {
983 let mut sim = ProgramSimulator::new(OrderingModel::new());
984 sim.init();
985
986 sim.send(OrderingMsg::StartBatch);
987 sim.capture_frame(1, 1);
988
989 assert_eq!(
990 sim.model().trace(),
991 vec!["batch-1", "batch-2", "batch-3", "view"]
992 );
993 }
994
995 #[test]
996 fn frame_dimensions_match_request() {
997 let mut sim = ProgramSimulator::new(Counter {
998 value: 42,
999 initialized: false,
1000 });
1001 sim.init();
1002
1003 let buf = sim.capture_frame(100, 50);
1004 assert_eq!(buf.width(), 100);
1005 assert_eq!(buf.height(), 50);
1006 }
1007
1008 #[test]
1009 fn multiple_frame_captures_are_independent() {
1010 let mut sim = ProgramSimulator::new(Counter {
1011 value: 0,
1012 initialized: false,
1013 });
1014 sim.init();
1015
1016 sim.capture_frame(20, 10);
1018
1019 sim.send(CounterMsg::Increment);
1021 sim.send(CounterMsg::Increment);
1022
1023 sim.capture_frame(20, 10);
1025
1026 let frames = sim.frames();
1027 assert_eq!(frames.len(), 2);
1028
1029 assert_eq!(frames[0].get(7, 0).unwrap().content.as_char(), Some('0'));
1031
1032 assert_eq!(frames[1].get(7, 0).unwrap().content.as_char(), Some('2'));
1034 }
1035
1036 #[test]
1037 fn inject_events_processes_in_order() {
1038 let mut sim = ProgramSimulator::new(Counter {
1039 value: 0,
1040 initialized: false,
1041 });
1042 sim.init();
1043
1044 let events = vec![
1046 key_event('+'),
1047 key_event('+'),
1048 key_event('+'),
1049 key_event('-'),
1050 key_event('+'),
1051 ];
1052
1053 sim.inject_events(&events);
1054
1055 assert_eq!(sim.model().value, 3);
1057 }
1058
1059 #[test]
1060 fn task_command_records_task() {
1061 struct TaskModel {
1062 result: Option<i32>,
1063 }
1064
1065 #[derive(Debug)]
1066 enum TaskMsg {
1067 SetResult(i32),
1068 SpawnTask,
1069 }
1070
1071 impl From<Event> for TaskMsg {
1072 fn from(_: Event) -> Self {
1073 TaskMsg::SetResult(0)
1074 }
1075 }
1076
1077 impl Model for TaskModel {
1078 type Message = TaskMsg;
1079
1080 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1081 match msg {
1082 TaskMsg::SetResult(v) => {
1083 self.result = Some(v);
1084 Cmd::none()
1085 }
1086 TaskMsg::SpawnTask => Cmd::task(|| {
1087 TaskMsg::SetResult(42)
1089 }),
1090 }
1091 }
1092
1093 fn view(&self, _frame: &mut Frame) {}
1094 }
1095
1096 let mut sim = ProgramSimulator::new(TaskModel { result: None });
1097 sim.init();
1098 sim.send(TaskMsg::SpawnTask);
1099
1100 assert_eq!(sim.model().result, Some(42));
1102
1103 let has_task = sim
1105 .command_log()
1106 .iter()
1107 .any(|r| matches!(r, CmdRecord::Task));
1108 assert!(has_task);
1109 }
1110
1111 #[test]
1112 fn tick_rate_is_set() {
1113 let mut sim = ProgramSimulator::new(Counter {
1114 value: 0,
1115 initialized: false,
1116 });
1117
1118 assert!(sim.tick_rate().is_none());
1119
1120 sim.execute_cmd(Cmd::tick(std::time::Duration::from_millis(100)));
1121
1122 assert_eq!(sim.tick_rate(), Some(std::time::Duration::from_millis(100)));
1123 }
1124
1125 #[test]
1126 fn logs_accumulate_across_messages() {
1127 let mut sim = ProgramSimulator::new(Counter {
1128 value: 0,
1129 initialized: false,
1130 });
1131 sim.init();
1132
1133 sim.send(CounterMsg::LogValue);
1134 sim.send(CounterMsg::Increment);
1135 sim.send(CounterMsg::LogValue);
1136 sim.send(CounterMsg::Increment);
1137 sim.send(CounterMsg::LogValue);
1138
1139 assert_eq!(sim.logs().len(), 3);
1140 assert_eq!(sim.logs()[0], "value=0");
1141 assert_eq!(sim.logs()[1], "value=1");
1142 assert_eq!(sim.logs()[2], "value=2");
1143 }
1144
1145 #[test]
1146 fn deterministic_frame_content_across_runs() {
1147 fn capture_frame_content(value: i32) -> Vec<Option<char>> {
1148 let mut sim = ProgramSimulator::new(Counter {
1149 value,
1150 initialized: false,
1151 });
1152 sim.init();
1153
1154 let buf = sim.capture_frame(15, 1);
1155 (0..15)
1156 .map(|x| buf.get(x, 0).and_then(|c| c.content.as_char()))
1157 .collect()
1158 }
1159
1160 let content1 = capture_frame_content(123);
1161 let content2 = capture_frame_content(123);
1162 let content3 = capture_frame_content(123);
1163
1164 assert_eq!(content1, content2);
1165 assert_eq!(content2, content3);
1166
1167 let expected: Vec<Option<char>> = "Count: 123"
1169 .chars()
1170 .map(Some)
1171 .chain(std::iter::repeat_n(None, 5))
1172 .collect();
1173 assert_eq!(content1, expected);
1174 }
1175
1176 #[test]
1177 fn complex_scenario_is_deterministic() {
1178 fn run_complex_scenario() -> (i32, usize, Vec<String>) {
1179 let mut sim = ProgramSimulator::new(Counter {
1180 value: 0,
1181 initialized: false,
1182 });
1183 sim.init();
1184
1185 for _ in 0..10 {
1187 sim.send(CounterMsg::Increment);
1188 }
1189 sim.send(CounterMsg::LogValue);
1190
1191 sim.send(CounterMsg::BatchIncrement(5));
1192 sim.send(CounterMsg::LogValue);
1193
1194 for _ in 0..3 {
1195 sim.send(CounterMsg::Decrement);
1196 }
1197 sim.send(CounterMsg::LogValue);
1198
1199 sim.send(CounterMsg::Reset);
1200 sim.send(CounterMsg::LogValue);
1201
1202 sim.capture_frame(20, 10);
1203
1204 (
1205 sim.model().value,
1206 sim.command_log().len(),
1207 sim.logs().to_vec(),
1208 )
1209 }
1210
1211 let result1 = run_complex_scenario();
1212 let result2 = run_complex_scenario();
1213
1214 assert_eq!(result1.0, result2.0);
1215 assert_eq!(result1.1, result2.1);
1216 assert_eq!(result1.2, result2.2);
1217 }
1218
1219 #[test]
1220 fn model_unchanged_when_not_running() {
1221 let mut sim = ProgramSimulator::new(Counter {
1222 value: 5,
1223 initialized: false,
1224 });
1225 sim.init();
1226
1227 sim.send(CounterMsg::Quit);
1228
1229 let value_before = sim.model().value;
1230 sim.send(CounterMsg::Increment);
1231 sim.send(CounterMsg::BatchIncrement(10));
1232 let value_after = sim.model().value;
1233
1234 assert_eq!(value_before, value_after);
1235 }
1236
1237 #[test]
1238 fn init_produces_consistent_command_log() {
1239 struct InitModel {
1241 init_ran: bool,
1242 }
1243
1244 #[derive(Debug)]
1245 enum InitMsg {
1246 MarkInit,
1247 }
1248
1249 impl From<Event> for InitMsg {
1250 fn from(_: Event) -> Self {
1251 InitMsg::MarkInit
1252 }
1253 }
1254
1255 impl Model for InitModel {
1256 type Message = InitMsg;
1257
1258 fn init(&mut self) -> Cmd<Self::Message> {
1259 Cmd::msg(InitMsg::MarkInit)
1260 }
1261
1262 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1263 match msg {
1264 InitMsg::MarkInit => {
1265 self.init_ran = true;
1266 Cmd::none()
1267 }
1268 }
1269 }
1270
1271 fn view(&self, _frame: &mut Frame) {}
1272 }
1273
1274 let mut sim1 = ProgramSimulator::new(InitModel { init_ran: false });
1275 let mut sim2 = ProgramSimulator::new(InitModel { init_ran: false });
1276
1277 sim1.init();
1278 sim2.init();
1279
1280 assert_eq!(sim1.model().init_ran, sim2.model().init_ran);
1281 assert_eq!(sim1.command_log().len(), sim2.command_log().len());
1282 }
1283
1284 #[test]
1285 fn execute_cmd_directly() {
1286 let mut sim = ProgramSimulator::new(Counter {
1287 value: 0,
1288 initialized: false,
1289 });
1290
1291 sim.execute_cmd(Cmd::log("direct log"));
1293 sim.execute_cmd(Cmd::tick(std::time::Duration::from_secs(1)));
1294
1295 assert_eq!(sim.logs(), &["direct log"]);
1296 assert_eq!(sim.tick_rate(), Some(std::time::Duration::from_secs(1)));
1297 }
1298
1299 #[test]
1300 fn save_restore_are_noops_in_simulator() {
1301 let mut sim = ProgramSimulator::new(Counter {
1302 value: 7,
1303 initialized: false,
1304 });
1305 sim.init();
1306
1307 let log_len = sim.command_log().len();
1308 let tick_rate = sim.tick_rate();
1309 let value_before = sim.model().value;
1310
1311 sim.execute_cmd(Cmd::save_state());
1312 sim.execute_cmd(Cmd::restore_state());
1313
1314 assert_eq!(sim.command_log().len(), log_len);
1315 assert_eq!(sim.tick_rate(), tick_rate);
1316 assert_eq!(sim.model().value, value_before);
1317 assert!(sim.is_running());
1318 }
1319
1320 #[test]
1321 fn grapheme_pool_is_reused() {
1322 let mut sim = ProgramSimulator::new(Counter {
1323 value: 0,
1324 initialized: false,
1325 });
1326 sim.init();
1327
1328 for i in 0..10 {
1330 sim.model_mut().value = i;
1331 sim.capture_frame(80, 24);
1332 }
1333
1334 assert_eq!(sim.frame_count(), 10);
1335 }
1336}