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