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