1#![forbid(unsafe_code)]
2
3use crate::program::{Cmd, Model};
23use crate::state_persistence::StateRegistry;
24use crate::subscription::SubId;
25use ftui_core::event::Event;
26use ftui_render::buffer::Buffer;
27use ftui_render::frame::Frame;
28use ftui_render::grapheme_pool::GraphemePool;
29use std::sync::Arc;
30use std::time::Duration;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum SimulatorError {
35 NotRunning,
37 InactiveSubscription(SubId),
39}
40
41impl std::fmt::Display for SimulatorError {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 Self::NotRunning => write!(f, "simulator is not running"),
45 Self::InactiveSubscription(id) => {
46 write!(f, "subscription {id} is not active")
47 }
48 }
49 }
50}
51
52impl std::error::Error for SimulatorError {}
53
54#[derive(Debug, Clone)]
56pub enum CmdRecord {
57 None,
59 Quit,
61 Msg,
63 Batch(usize),
65 Sequence(usize),
67 Tick(Duration),
69 Log(String),
71 Task,
73 MouseCapture(bool),
75 Error(String),
77 Shutdown,
79}
80
81pub struct ProgramSimulator<M: Model> {
86 model: M,
88 pool: GraphemePool,
90 frames: Vec<Buffer>,
92 command_log: Vec<CmdRecord>,
94 running: bool,
96 initialized: bool,
98 shutdown_complete: bool,
100 handling_error: bool,
102 tick_rate: Option<Duration>,
104 now: Duration,
106 next_tick_at: Option<Duration>,
108 active_subscriptions: Vec<SubId>,
110 logs: Vec<String>,
112 errors: Vec<String>,
114 state_registry: Option<Arc<StateRegistry>>,
116}
117
118impl<M: Model> ProgramSimulator<M> {
119 pub fn new(model: M) -> Self {
123 Self {
124 model,
125 pool: GraphemePool::new(),
126 frames: Vec::new(),
127 command_log: Vec::new(),
128 running: true,
129 initialized: false,
130 shutdown_complete: false,
131 handling_error: false,
132 tick_rate: None,
133 now: Duration::ZERO,
134 next_tick_at: None,
135 active_subscriptions: Vec::new(),
136 logs: Vec::new(),
137 errors: Vec::new(),
138 state_registry: None,
139 }
140 }
141
142 pub fn with_registry(model: M, registry: Arc<StateRegistry>) -> Self {
147 let mut sim = Self::new(model);
148 sim.state_registry = Some(registry);
149 sim
150 }
151
152 pub fn init(&mut self) {
156 if self.initialized || self.shutdown_complete {
157 return;
158 }
159 self.initialized = true;
160 let cmd = self.model.init();
161 self.execute_cmd(cmd);
162 self.poll_subscriptions();
163 }
164
165 pub fn inject_events(&mut self, events: &[Event]) {
170 for event in events {
171 if !self.running {
172 break;
173 }
174 let msg = M::Message::from(event.clone());
175 let cmd = self.model.update(msg);
176 self.execute_cmd(cmd);
177 self.poll_subscriptions();
178 }
179 }
180
181 pub fn inject_event(&mut self, event: Event) {
186 self.inject_events(&[event]);
187 }
188
189 pub fn send(&mut self, msg: M::Message) {
194 if !self.running {
195 return;
196 }
197 let cmd = self.model.update(msg);
198 self.execute_cmd(cmd);
199 self.poll_subscriptions();
200 }
201
202 #[inline]
204 pub fn now(&self) -> Duration {
205 self.now
206 }
207
208 pub fn advance_time(&mut self, delta: Duration) -> usize {
213 let target = self.now.saturating_add(delta);
214 let mut delivered = 0_usize;
215
216 while self.running {
217 let Some(due) = self.next_tick_at else {
218 break;
219 };
220 if due > target {
221 break;
222 }
223
224 self.now = due;
225 self.next_tick_at = self
226 .tick_rate
227 .filter(|rate| !rate.is_zero())
228 .and_then(|rate| due.checked_add(rate));
229 self.send(M::Message::from(Event::Tick));
230 delivered = delivered.saturating_add(1);
231 }
232
233 self.now = target;
234 delivered
235 }
236
237 pub fn tick(&mut self) {
239 if self.running {
240 self.send(M::Message::from(Event::Tick));
241 }
242 }
243
244 pub fn poll_subscriptions(&mut self) -> &[SubId] {
251 if self.shutdown_complete {
252 self.active_subscriptions.clear();
253 return &self.active_subscriptions;
254 }
255
256 let mut ids: Vec<_> = self
257 .model
258 .subscriptions()
259 .into_iter()
260 .map(|subscription| subscription.id())
261 .collect();
262 ids.sort_unstable();
263 ids.dedup();
264 self.active_subscriptions = ids;
265 &self.active_subscriptions
266 }
267
268 #[inline]
270 pub fn active_subscription_ids(&self) -> &[SubId] {
271 &self.active_subscriptions
272 }
273
274 pub fn deliver_subscription(
276 &mut self,
277 id: SubId,
278 msg: M::Message,
279 ) -> Result<(), SimulatorError> {
280 if !self.running {
281 return Err(SimulatorError::NotRunning);
282 }
283 if !self.active_subscriptions.contains(&id) {
284 let error = SimulatorError::InactiveSubscription(id);
285 self.report_error(error.to_string());
286 return Err(error);
287 }
288
289 let cmd = self.model.update(msg);
290 self.execute_cmd(cmd);
291 self.poll_subscriptions();
292 Ok(())
293 }
294
295 pub fn report_error(&mut self, error: impl Into<String>) {
297 let error = error.into();
298 self.errors.push(error.clone());
299 self.command_log.push(CmdRecord::Error(error.clone()));
300
301 if self.handling_error || self.shutdown_complete {
302 return;
303 }
304
305 self.handling_error = true;
306 let cmd = self.model.on_error(&error);
307 self.execute_lifecycle_cmd(cmd);
308 self.handling_error = false;
309 self.poll_subscriptions();
310 }
311
312 pub fn cancel(&mut self) {
314 self.running = false;
315 self.shutdown();
316 }
317
318 pub fn shutdown(&mut self) {
320 if self.shutdown_complete {
321 return;
322 }
323 self.shutdown_complete = true;
324 self.running = false;
325 self.command_log.push(CmdRecord::Shutdown);
326 let cmd = self.model.on_shutdown();
327 self.execute_lifecycle_cmd(cmd);
328 self.active_subscriptions.clear();
329 }
330
331 #[inline]
333 pub fn is_shutdown(&self) -> bool {
334 self.shutdown_complete
335 }
336
337 #[inline]
339 pub fn errors(&self) -> &[String] {
340 &self.errors
341 }
342
343 pub fn capture_frame(&mut self, width: u16, height: u16) -> &Buffer {
348 let mut frame = Frame::new(width, height, &mut self.pool);
349 self.model.view(&mut frame);
350 self.frames.push(frame.buffer);
351 self.frames.last().expect("frame just pushed")
352 }
353
354 pub fn frames(&self) -> &[Buffer] {
356 &self.frames
357 }
358
359 pub fn last_frame(&self) -> Option<&Buffer> {
361 self.frames.last()
362 }
363
364 pub fn frame_count(&self) -> usize {
366 self.frames.len()
367 }
368
369 #[inline]
371 pub fn model(&self) -> &M {
372 &self.model
373 }
374
375 #[inline]
377 pub fn model_mut(&mut self) -> &mut M {
378 &mut self.model
379 }
380
381 #[inline]
383 pub fn pool(&self) -> &GraphemePool {
384 &self.pool
385 }
386
387 #[inline]
391 pub fn is_running(&self) -> bool {
392 self.running
393 }
394
395 #[inline]
397 pub fn tick_rate(&self) -> Option<Duration> {
398 self.tick_rate
399 }
400
401 #[inline]
403 pub fn logs(&self) -> &[String] {
404 &self.logs
405 }
406
407 #[inline]
409 pub fn command_log(&self) -> &[CmdRecord] {
410 &self.command_log
411 }
412
413 pub fn clear_frames(&mut self) {
415 self.frames.clear();
416 }
417
418 pub fn clear_logs(&mut self) {
420 self.logs.clear();
421 }
422
423 fn execute_lifecycle_cmd(&mut self, cmd: Cmd<M::Message>) {
425 let was_running = std::mem::replace(&mut self.running, true);
426 self.execute_cmd(cmd);
427 self.running = was_running && self.running;
428 }
429
430 fn execute_cmd(&mut self, cmd: Cmd<M::Message>) {
436 match cmd {
437 Cmd::None => {
438 self.command_log.push(CmdRecord::None);
439 }
440 Cmd::Quit => {
441 self.running = false;
442 self.command_log.push(CmdRecord::Quit);
443 }
444 Cmd::Msg(m) => {
445 self.command_log.push(CmdRecord::Msg);
446 let cmd = self.model.update(m);
447 self.execute_cmd(cmd);
448 }
449 Cmd::Batch(cmds) => {
450 let count = cmds.len();
451 self.command_log.push(CmdRecord::Batch(count));
452 for c in cmds {
453 self.execute_cmd(c);
454 if !self.running {
455 break;
456 }
457 }
458 }
459 Cmd::Sequence(cmds) => {
460 let count = cmds.len();
461 self.command_log.push(CmdRecord::Sequence(count));
462 for c in cmds {
463 self.execute_cmd(c);
464 if !self.running {
465 break;
466 }
467 }
468 }
469 Cmd::Tick(duration) => {
470 self.tick_rate = Some(duration);
471 self.next_tick_at = if duration.is_zero() {
472 None
473 } else {
474 self.now.checked_add(duration)
475 };
476 self.command_log.push(CmdRecord::Tick(duration));
477 }
478 Cmd::Log(text) => {
479 self.command_log.push(CmdRecord::Log(text.clone()));
480 self.logs.push(text);
481 }
482 Cmd::SetMouseCapture(enabled) => {
483 self.command_log.push(CmdRecord::MouseCapture(enabled));
484 }
485 Cmd::Task(_, f) => {
486 self.command_log.push(CmdRecord::Task);
487 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
488 Ok(msg) => {
489 let cmd = self.model.update(msg);
490 self.execute_cmd(cmd);
491 }
492 Err(payload) => {
493 let error = if let Some(message) = payload.downcast_ref::<&str>() {
494 (*message).to_owned()
495 } else if let Some(message) = payload.downcast_ref::<String>() {
496 message.clone()
497 } else {
498 "background task panicked with a non-string payload".to_owned()
499 };
500 self.report_error(format!("background task failed: {error}"));
501 }
502 }
503 }
504 Cmd::SaveState => {
505 let error = self
506 .state_registry
507 .as_ref()
508 .and_then(|registry| registry.flush().err())
509 .map(|error| format!("state save failed: {error}"));
510 if let Some(error) = error {
511 self.report_error(error);
512 }
513 }
514 Cmd::RestoreState => {
515 let error = self
516 .state_registry
517 .as_ref()
518 .and_then(|registry| registry.load().err())
519 .map(|error| format!("state restore failed: {error}"));
520 if let Some(error) = error {
521 self.report_error(error);
522 }
523 }
524 Cmd::SetTickStrategy(_) => {
525 }
527 }
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534 use ftui_core::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
535 use std::cell::RefCell;
536 use std::sync::Arc;
537
538 struct Counter {
541 value: i32,
542 initialized: bool,
543 }
544
545 #[derive(Debug)]
546 enum CounterMsg {
547 Increment,
548 Decrement,
549 Reset,
550 Quit,
551 LogValue,
552 BatchIncrement(usize),
553 }
554
555 impl From<Event> for CounterMsg {
556 fn from(event: Event) -> Self {
557 match event {
558 Event::Key(k) if k.code == KeyCode::Char('+') => CounterMsg::Increment,
559 Event::Key(k) if k.code == KeyCode::Char('-') => CounterMsg::Decrement,
560 Event::Key(k) if k.code == KeyCode::Char('r') => CounterMsg::Reset,
561 Event::Key(k) if k.code == KeyCode::Char('q') => CounterMsg::Quit,
562 _ => CounterMsg::Increment,
563 }
564 }
565 }
566
567 impl Model for Counter {
568 type Message = CounterMsg;
569
570 fn init(&mut self) -> Cmd<Self::Message> {
571 self.initialized = true;
572 Cmd::none()
573 }
574
575 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
576 match msg {
577 CounterMsg::Increment => {
578 self.value += 1;
579 Cmd::none()
580 }
581 CounterMsg::Decrement => {
582 self.value -= 1;
583 Cmd::none()
584 }
585 CounterMsg::Reset => {
586 self.value = 0;
587 Cmd::none()
588 }
589 CounterMsg::Quit => Cmd::quit(),
590 CounterMsg::LogValue => Cmd::log(format!("value={}", self.value)),
591 CounterMsg::BatchIncrement(n) => {
592 let cmds: Vec<_> = (0..n).map(|_| Cmd::msg(CounterMsg::Increment)).collect();
593 Cmd::batch(cmds)
594 }
595 }
596 }
597
598 fn view(&self, frame: &mut Frame) {
599 let text = format!("Count: {}", self.value);
601 for (i, c) in text.chars().enumerate() {
602 if (i as u16) < frame.width() {
603 use ftui_render::cell::Cell;
604 frame.buffer.set_raw(i as u16, 0, Cell::from_char(c));
605 }
606 }
607 }
608 }
609
610 fn key_event(c: char) -> Event {
611 Event::Key(KeyEvent {
612 code: KeyCode::Char(c),
613 modifiers: Modifiers::empty(),
614 kind: KeyEventKind::Press,
615 })
616 }
617
618 fn resize_event(width: u16, height: u16) -> Event {
619 Event::Resize { width, height }
620 }
621
622 #[derive(Default)]
623 struct ResizeTracker {
624 last: Option<(u16, u16)>,
625 history: Vec<(u16, u16)>,
626 }
627
628 #[derive(Debug, Clone, Copy)]
629 enum ResizeMsg {
630 Resize(u16, u16),
631 Quit,
632 Noop,
633 }
634
635 impl From<Event> for ResizeMsg {
636 fn from(event: Event) -> Self {
637 match event {
638 Event::Resize { width, height } => Self::Resize(width, height),
639 Event::Key(k) if k.code == KeyCode::Char('q') => Self::Quit,
640 _ => Self::Noop,
641 }
642 }
643 }
644
645 impl Model for ResizeTracker {
646 type Message = ResizeMsg;
647
648 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
649 match msg {
650 ResizeMsg::Resize(width, height) => {
651 self.last = Some((width, height));
652 self.history.push((width, height));
653 Cmd::none()
654 }
655 ResizeMsg::Quit => Cmd::quit(),
656 ResizeMsg::Noop => Cmd::none(),
657 }
658 }
659
660 fn view(&self, _frame: &mut Frame) {}
661 }
662
663 #[derive(Default)]
664 struct PersistModel;
665
666 #[derive(Debug, Clone, Copy)]
667 enum PersistMsg {
668 Save,
669 Restore,
670 Noop,
671 }
672
673 impl From<Event> for PersistMsg {
674 fn from(event: Event) -> Self {
675 match event {
676 Event::Key(k) if k.code == KeyCode::Char('s') => Self::Save,
677 Event::Key(k) if k.code == KeyCode::Char('r') => Self::Restore,
678 _ => Self::Noop,
679 }
680 }
681 }
682
683 impl Model for PersistModel {
684 type Message = PersistMsg;
685
686 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
687 match msg {
688 PersistMsg::Save => Cmd::save_state(),
689 PersistMsg::Restore => Cmd::restore_state(),
690 PersistMsg::Noop => Cmd::none(),
691 }
692 }
693
694 fn view(&self, _frame: &mut Frame) {}
695 }
696
697 #[test]
700 fn new_simulator() {
701 let sim = ProgramSimulator::new(Counter {
702 value: 0,
703 initialized: false,
704 });
705 assert!(sim.is_running());
706 assert_eq!(sim.model().value, 0);
707 assert!(!sim.model().initialized);
708 assert_eq!(sim.frame_count(), 0);
709 assert!(sim.logs().is_empty());
710 }
711
712 #[test]
713 fn init_calls_model_init() {
714 let mut sim = ProgramSimulator::new(Counter {
715 value: 0,
716 initialized: false,
717 });
718 sim.init();
719 assert!(sim.model().initialized);
720 }
721
722 #[test]
723 fn inject_events_processes_all() {
724 let mut sim = ProgramSimulator::new(Counter {
725 value: 0,
726 initialized: false,
727 });
728 sim.init();
729
730 let events = vec![key_event('+'), key_event('+'), key_event('+')];
731 sim.inject_events(&events);
732
733 assert_eq!(sim.model().value, 3);
734 }
735
736 #[test]
737 fn inject_events_stops_on_quit() {
738 let mut sim = ProgramSimulator::new(Counter {
739 value: 0,
740 initialized: false,
741 });
742 sim.init();
743
744 let events = vec![key_event('+'), key_event('q'), key_event('+')];
746 sim.inject_events(&events);
747
748 assert_eq!(sim.model().value, 1);
749 assert!(!sim.is_running());
750 }
751
752 #[test]
753 fn save_state_flushes_registry() {
754 use crate::state_persistence::StateRegistry;
755
756 let registry = Arc::new(StateRegistry::in_memory());
757 registry.set("viewer", 1, vec![1, 2, 3]);
758 assert!(registry.is_dirty());
759
760 let mut sim = ProgramSimulator::with_registry(PersistModel, Arc::clone(®istry));
761 sim.send(PersistMsg::Save);
762
763 assert!(!registry.is_dirty());
764 let stored = registry.get("viewer").expect("entry present");
765 assert_eq!(stored.version, 1);
766 assert_eq!(stored.data, vec![1, 2, 3]);
767 }
768
769 #[test]
770 fn restore_state_round_trips_cache() {
771 use crate::state_persistence::StateRegistry;
772
773 let registry = Arc::new(StateRegistry::in_memory());
774 registry.set("viewer", 7, vec![9, 8, 7]);
775
776 let mut sim = ProgramSimulator::with_registry(PersistModel, Arc::clone(®istry));
777 sim.send(PersistMsg::Save);
778
779 let removed = registry.remove("viewer");
780 assert!(removed.is_some());
781 assert!(registry.get("viewer").is_none());
782
783 sim.send(PersistMsg::Restore);
784 let restored = registry.get("viewer").expect("restored entry");
785 assert_eq!(restored.version, 7);
786 assert_eq!(restored.data, vec![9, 8, 7]);
787 }
788
789 #[test]
790 fn resize_events_apply_in_order() {
791 let mut sim = ProgramSimulator::new(ResizeTracker::default());
792 sim.init();
793
794 let events = vec![
795 resize_event(80, 24),
796 resize_event(100, 40),
797 resize_event(120, 50),
798 ];
799 sim.inject_events(&events);
800
801 assert_eq!(sim.model().history, vec![(80, 24), (100, 40), (120, 50)]);
802 assert_eq!(sim.model().last, Some((120, 50)));
803 }
804
805 #[test]
806 fn resize_events_after_quit_are_ignored() {
807 let mut sim = ProgramSimulator::new(ResizeTracker::default());
808 sim.init();
809
810 let events = vec![resize_event(80, 24), key_event('q'), resize_event(120, 50)];
811 sim.inject_events(&events);
812
813 assert!(!sim.is_running());
814 assert_eq!(sim.model().history, vec![(80, 24)]);
815 assert_eq!(sim.model().last, Some((80, 24)));
816 }
817
818 #[test]
819 fn send_message_directly() {
820 let mut sim = ProgramSimulator::new(Counter {
821 value: 0,
822 initialized: false,
823 });
824 sim.init();
825
826 sim.send(CounterMsg::Increment);
827 sim.send(CounterMsg::Increment);
828 sim.send(CounterMsg::Decrement);
829
830 assert_eq!(sim.model().value, 1);
831 }
832
833 #[test]
834 fn capture_frame_renders_correctly() {
835 let mut sim = ProgramSimulator::new(Counter {
836 value: 42,
837 initialized: false,
838 });
839 sim.init();
840
841 let buf = sim.capture_frame(80, 24);
842
843 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('C'));
845 assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('o'));
846 assert_eq!(buf.get(7, 0).unwrap().content.as_char(), Some('4'));
847 assert_eq!(buf.get(8, 0).unwrap().content.as_char(), Some('2'));
848 }
849
850 #[test]
851 fn multiple_frame_captures() {
852 let mut sim = ProgramSimulator::new(Counter {
853 value: 0,
854 initialized: false,
855 });
856 sim.init();
857
858 sim.capture_frame(80, 24);
859 sim.send(CounterMsg::Increment);
860 sim.capture_frame(80, 24);
861
862 assert_eq!(sim.frame_count(), 2);
863
864 assert_eq!(
866 sim.frames()[0].get(7, 0).unwrap().content.as_char(),
867 Some('0')
868 );
869 assert_eq!(
871 sim.frames()[1].get(7, 0).unwrap().content.as_char(),
872 Some('1')
873 );
874 }
875
876 #[test]
877 fn quit_command_stops_running() {
878 let mut sim = ProgramSimulator::new(Counter {
879 value: 0,
880 initialized: false,
881 });
882 sim.init();
883
884 assert!(sim.is_running());
885 sim.send(CounterMsg::Quit);
886 assert!(!sim.is_running());
887 }
888
889 #[test]
890 fn log_command_records_text() {
891 let mut sim = ProgramSimulator::new(Counter {
892 value: 5,
893 initialized: false,
894 });
895 sim.init();
896
897 sim.send(CounterMsg::LogValue);
898
899 assert_eq!(sim.logs(), &["value=5"]);
900 }
901
902 #[test]
903 fn batch_command_executes_all() {
904 let mut sim = ProgramSimulator::new(Counter {
905 value: 0,
906 initialized: false,
907 });
908 sim.init();
909
910 sim.send(CounterMsg::BatchIncrement(5));
911
912 assert_eq!(sim.model().value, 5);
913 }
914
915 #[test]
916 fn tick_command_sets_rate() {
917 let mut sim = ProgramSimulator::new(Counter {
918 value: 0,
919 initialized: false,
920 });
921
922 assert!(sim.tick_rate().is_none());
923
924 sim.execute_cmd(Cmd::tick(Duration::from_millis(100)));
929
930 assert_eq!(sim.tick_rate(), Some(Duration::from_millis(100)));
931 }
932
933 #[test]
934 fn deterministic_clock_stops_when_next_deadline_would_overflow() {
935 let mut sim = ProgramSimulator::new(Counter {
936 value: 0,
937 initialized: false,
938 });
939 sim.execute_cmd(Cmd::tick(Duration::MAX));
940
941 assert_eq!(sim.advance_time(Duration::MAX), 1);
942 assert_eq!(sim.now(), Duration::MAX);
943 assert_eq!(sim.advance_time(Duration::ZERO), 0);
944 }
945
946 #[test]
947 fn command_log_records_all() {
948 let mut sim = ProgramSimulator::new(Counter {
949 value: 0,
950 initialized: false,
951 });
952 sim.init();
953
954 sim.send(CounterMsg::Increment);
955 sim.send(CounterMsg::Quit);
956
957 assert!(sim.command_log().len() >= 3);
959 assert!(matches!(sim.command_log().last(), Some(CmdRecord::Quit)));
960 }
961
962 #[test]
963 fn clear_frames() {
964 let mut sim = ProgramSimulator::new(Counter {
965 value: 0,
966 initialized: false,
967 });
968 sim.capture_frame(10, 10);
969 sim.capture_frame(10, 10);
970 assert_eq!(sim.frame_count(), 2);
971
972 sim.clear_frames();
973 assert_eq!(sim.frame_count(), 0);
974 }
975
976 #[test]
977 fn clear_logs() {
978 let mut sim = ProgramSimulator::new(Counter {
979 value: 0,
980 initialized: false,
981 });
982 sim.init();
983 sim.send(CounterMsg::LogValue);
984 assert_eq!(sim.logs().len(), 1);
985
986 sim.clear_logs();
987 assert!(sim.logs().is_empty());
988 }
989
990 #[test]
991 fn model_mut_access() {
992 let mut sim = ProgramSimulator::new(Counter {
993 value: 0,
994 initialized: false,
995 });
996
997 sim.model_mut().value = 100;
998 assert_eq!(sim.model().value, 100);
999 }
1000
1001 #[test]
1002 fn last_frame() {
1003 let mut sim = ProgramSimulator::new(Counter {
1004 value: 0,
1005 initialized: false,
1006 });
1007
1008 assert!(sim.last_frame().is_none());
1009
1010 sim.capture_frame(10, 10);
1011 assert!(sim.last_frame().is_some());
1012 }
1013
1014 #[test]
1015 fn send_after_quit_is_ignored() {
1016 let mut sim = ProgramSimulator::new(Counter {
1017 value: 0,
1018 initialized: false,
1019 });
1020 sim.init();
1021
1022 sim.send(CounterMsg::Quit);
1023 assert!(!sim.is_running());
1024
1025 sim.send(CounterMsg::Increment);
1026 assert_eq!(sim.model().value, 0);
1028 }
1029
1030 #[test]
1035 fn identical_inputs_yield_identical_outputs() {
1036 fn run_scenario() -> (i32, Vec<u8>) {
1037 let mut sim = ProgramSimulator::new(Counter {
1038 value: 0,
1039 initialized: false,
1040 });
1041 sim.init();
1042
1043 sim.send(CounterMsg::Increment);
1044 sim.send(CounterMsg::Increment);
1045 sim.send(CounterMsg::Decrement);
1046 sim.send(CounterMsg::BatchIncrement(3));
1047
1048 let buf = sim.capture_frame(20, 10);
1049 let mut frame_bytes = Vec::new();
1050 for y in 0..10 {
1051 for x in 0..20 {
1052 if let Some(cell) = buf.get(x, y)
1053 && let Some(c) = cell.content.as_char()
1054 {
1055 frame_bytes.push(c as u8);
1056 }
1057 }
1058 }
1059 (sim.model().value, frame_bytes)
1060 }
1061
1062 let (value1, frame1) = run_scenario();
1063 let (value2, frame2) = run_scenario();
1064 let (value3, frame3) = run_scenario();
1065
1066 assert_eq!(value1, value2);
1067 assert_eq!(value2, value3);
1068 assert_eq!(value1, 4); assert_eq!(frame1, frame2);
1071 assert_eq!(frame2, frame3);
1072 }
1073
1074 #[test]
1075 fn command_log_records_in_order() {
1076 let mut sim = ProgramSimulator::new(Counter {
1077 value: 0,
1078 initialized: false,
1079 });
1080 sim.init();
1081
1082 sim.send(CounterMsg::Increment);
1083 sim.send(CounterMsg::LogValue);
1084 sim.send(CounterMsg::Increment);
1085 sim.send(CounterMsg::LogValue);
1086
1087 let log = sim.command_log();
1088
1089 let log_entries: Vec<_> = log
1091 .iter()
1092 .filter_map(|r| {
1093 if let CmdRecord::Log(s) = r {
1094 Some(s.as_str())
1095 } else {
1096 None
1097 }
1098 })
1099 .collect();
1100
1101 assert_eq!(log_entries, vec!["value=1", "value=2"]);
1102 }
1103
1104 #[test]
1105 fn sequence_command_records_correctly() {
1106 struct SeqModel {
1108 steps: Vec<i32>,
1109 }
1110
1111 #[derive(Debug)]
1112 enum SeqMsg {
1113 Step(i32),
1114 TriggerSeq,
1115 }
1116
1117 impl From<Event> for SeqMsg {
1118 fn from(_: Event) -> Self {
1119 SeqMsg::Step(0)
1120 }
1121 }
1122
1123 impl Model for SeqModel {
1124 type Message = SeqMsg;
1125
1126 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1127 match msg {
1128 SeqMsg::Step(n) => {
1129 self.steps.push(n);
1130 Cmd::none()
1131 }
1132 SeqMsg::TriggerSeq => Cmd::sequence(vec![
1133 Cmd::msg(SeqMsg::Step(1)),
1134 Cmd::msg(SeqMsg::Step(2)),
1135 Cmd::msg(SeqMsg::Step(3)),
1136 ]),
1137 }
1138 }
1139
1140 fn view(&self, _frame: &mut Frame) {}
1141 }
1142
1143 let mut sim = ProgramSimulator::new(SeqModel { steps: vec![] });
1144 sim.init();
1145 sim.send(SeqMsg::TriggerSeq);
1146
1147 let has_sequence = sim
1149 .command_log()
1150 .iter()
1151 .any(|r| matches!(r, CmdRecord::Sequence(3)));
1152 assert!(has_sequence, "Should record Sequence(3)");
1153
1154 assert_eq!(sim.model().steps, vec![1, 2, 3]);
1156 }
1157
1158 #[test]
1159 fn batch_command_records_correctly() {
1160 let mut sim = ProgramSimulator::new(Counter {
1161 value: 0,
1162 initialized: false,
1163 });
1164 sim.init();
1165
1166 sim.send(CounterMsg::BatchIncrement(5));
1167
1168 let has_batch = sim
1170 .command_log()
1171 .iter()
1172 .any(|r| matches!(r, CmdRecord::Batch(5)));
1173 assert!(has_batch, "Should record Batch(5)");
1174
1175 assert_eq!(sim.model().value, 5);
1176 }
1177
1178 struct OrderingModel {
1179 trace: RefCell<Vec<&'static str>>,
1180 }
1181
1182 impl OrderingModel {
1183 fn new() -> Self {
1184 Self {
1185 trace: RefCell::new(Vec::new()),
1186 }
1187 }
1188
1189 fn trace(&self) -> Vec<&'static str> {
1190 self.trace.borrow().clone()
1191 }
1192 }
1193
1194 #[derive(Debug)]
1195 enum OrderingMsg {
1196 Step(&'static str),
1197 StartSequence,
1198 StartBatch,
1199 }
1200
1201 impl From<Event> for OrderingMsg {
1202 fn from(_: Event) -> Self {
1203 OrderingMsg::StartSequence
1204 }
1205 }
1206
1207 impl Model for OrderingModel {
1208 type Message = OrderingMsg;
1209
1210 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1211 match msg {
1212 OrderingMsg::Step(tag) => {
1213 self.trace.borrow_mut().push(tag);
1214 Cmd::none()
1215 }
1216 OrderingMsg::StartSequence => Cmd::sequence(vec![
1217 Cmd::msg(OrderingMsg::Step("seq-1")),
1218 Cmd::msg(OrderingMsg::Step("seq-2")),
1219 Cmd::msg(OrderingMsg::Step("seq-3")),
1220 ]),
1221 OrderingMsg::StartBatch => Cmd::batch(vec![
1222 Cmd::msg(OrderingMsg::Step("batch-1")),
1223 Cmd::msg(OrderingMsg::Step("batch-2")),
1224 Cmd::msg(OrderingMsg::Step("batch-3")),
1225 ]),
1226 }
1227 }
1228
1229 fn view(&self, _frame: &mut Frame) {
1230 self.trace.borrow_mut().push("view");
1231 }
1232 }
1233
1234 #[test]
1235 fn sequence_preserves_update_order_before_view() {
1236 let mut sim = ProgramSimulator::new(OrderingModel::new());
1237 sim.init();
1238
1239 sim.send(OrderingMsg::StartSequence);
1240 sim.capture_frame(1, 1);
1241
1242 assert_eq!(sim.model().trace(), vec!["seq-1", "seq-2", "seq-3", "view"]);
1243 }
1244
1245 #[test]
1246 fn batch_preserves_update_order_before_view() {
1247 let mut sim = ProgramSimulator::new(OrderingModel::new());
1248 sim.init();
1249
1250 sim.send(OrderingMsg::StartBatch);
1251 sim.capture_frame(1, 1);
1252
1253 assert_eq!(
1254 sim.model().trace(),
1255 vec!["batch-1", "batch-2", "batch-3", "view"]
1256 );
1257 }
1258
1259 #[test]
1260 fn frame_dimensions_match_request() {
1261 let mut sim = ProgramSimulator::new(Counter {
1262 value: 42,
1263 initialized: false,
1264 });
1265 sim.init();
1266
1267 let buf = sim.capture_frame(100, 50);
1268 assert_eq!(buf.width(), 100);
1269 assert_eq!(buf.height(), 50);
1270 }
1271
1272 #[test]
1273 fn multiple_frame_captures_are_independent() {
1274 let mut sim = ProgramSimulator::new(Counter {
1275 value: 0,
1276 initialized: false,
1277 });
1278 sim.init();
1279
1280 sim.capture_frame(20, 10);
1282
1283 sim.send(CounterMsg::Increment);
1285 sim.send(CounterMsg::Increment);
1286
1287 sim.capture_frame(20, 10);
1289
1290 let frames = sim.frames();
1291 assert_eq!(frames.len(), 2);
1292
1293 assert_eq!(frames[0].get(7, 0).unwrap().content.as_char(), Some('0'));
1295
1296 assert_eq!(frames[1].get(7, 0).unwrap().content.as_char(), Some('2'));
1298 }
1299
1300 #[test]
1301 fn inject_events_processes_in_order() {
1302 let mut sim = ProgramSimulator::new(Counter {
1303 value: 0,
1304 initialized: false,
1305 });
1306 sim.init();
1307
1308 let events = vec![
1310 key_event('+'),
1311 key_event('+'),
1312 key_event('+'),
1313 key_event('-'),
1314 key_event('+'),
1315 ];
1316
1317 sim.inject_events(&events);
1318
1319 assert_eq!(sim.model().value, 3);
1321 }
1322
1323 #[test]
1324 fn task_command_records_task() {
1325 struct TaskModel {
1326 result: Option<i32>,
1327 }
1328
1329 #[derive(Debug)]
1330 enum TaskMsg {
1331 SetResult(i32),
1332 SpawnTask,
1333 }
1334
1335 impl From<Event> for TaskMsg {
1336 fn from(_: Event) -> Self {
1337 TaskMsg::SetResult(0)
1338 }
1339 }
1340
1341 impl Model for TaskModel {
1342 type Message = TaskMsg;
1343
1344 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1345 match msg {
1346 TaskMsg::SetResult(v) => {
1347 self.result = Some(v);
1348 Cmd::none()
1349 }
1350 TaskMsg::SpawnTask => Cmd::task(|| {
1351 TaskMsg::SetResult(42)
1353 }),
1354 }
1355 }
1356
1357 fn view(&self, _frame: &mut Frame) {}
1358 }
1359
1360 let mut sim = ProgramSimulator::new(TaskModel { result: None });
1361 sim.init();
1362 sim.send(TaskMsg::SpawnTask);
1363
1364 assert_eq!(sim.model().result, Some(42));
1366
1367 let has_task = sim
1369 .command_log()
1370 .iter()
1371 .any(|r| matches!(r, CmdRecord::Task));
1372 assert!(has_task);
1373 }
1374
1375 #[test]
1376 fn tick_rate_is_set() {
1377 let mut sim = ProgramSimulator::new(Counter {
1378 value: 0,
1379 initialized: false,
1380 });
1381
1382 assert!(sim.tick_rate().is_none());
1383
1384 sim.execute_cmd(Cmd::tick(std::time::Duration::from_millis(100)));
1385
1386 assert_eq!(sim.tick_rate(), Some(std::time::Duration::from_millis(100)));
1387 }
1388
1389 #[test]
1390 fn logs_accumulate_across_messages() {
1391 let mut sim = ProgramSimulator::new(Counter {
1392 value: 0,
1393 initialized: false,
1394 });
1395 sim.init();
1396
1397 sim.send(CounterMsg::LogValue);
1398 sim.send(CounterMsg::Increment);
1399 sim.send(CounterMsg::LogValue);
1400 sim.send(CounterMsg::Increment);
1401 sim.send(CounterMsg::LogValue);
1402
1403 assert_eq!(sim.logs().len(), 3);
1404 assert_eq!(sim.logs()[0], "value=0");
1405 assert_eq!(sim.logs()[1], "value=1");
1406 assert_eq!(sim.logs()[2], "value=2");
1407 }
1408
1409 #[test]
1410 fn deterministic_frame_content_across_runs() {
1411 fn capture_frame_content(value: i32) -> Vec<Option<char>> {
1412 let mut sim = ProgramSimulator::new(Counter {
1413 value,
1414 initialized: false,
1415 });
1416 sim.init();
1417
1418 let buf = sim.capture_frame(15, 1);
1419 (0..15)
1420 .map(|x| buf.get(x, 0).and_then(|c| c.content.as_char()))
1421 .collect()
1422 }
1423
1424 let content1 = capture_frame_content(123);
1425 let content2 = capture_frame_content(123);
1426 let content3 = capture_frame_content(123);
1427
1428 assert_eq!(content1, content2);
1429 assert_eq!(content2, content3);
1430
1431 let expected: Vec<Option<char>> = "Count: 123"
1433 .chars()
1434 .map(Some)
1435 .chain(std::iter::repeat_n(None, 5))
1436 .collect();
1437 assert_eq!(content1, expected);
1438 }
1439
1440 #[test]
1441 fn complex_scenario_is_deterministic() {
1442 fn run_complex_scenario() -> (i32, usize, Vec<String>) {
1443 let mut sim = ProgramSimulator::new(Counter {
1444 value: 0,
1445 initialized: false,
1446 });
1447 sim.init();
1448
1449 for _ in 0..10 {
1451 sim.send(CounterMsg::Increment);
1452 }
1453 sim.send(CounterMsg::LogValue);
1454
1455 sim.send(CounterMsg::BatchIncrement(5));
1456 sim.send(CounterMsg::LogValue);
1457
1458 for _ in 0..3 {
1459 sim.send(CounterMsg::Decrement);
1460 }
1461 sim.send(CounterMsg::LogValue);
1462
1463 sim.send(CounterMsg::Reset);
1464 sim.send(CounterMsg::LogValue);
1465
1466 sim.capture_frame(20, 10);
1467
1468 (
1469 sim.model().value,
1470 sim.command_log().len(),
1471 sim.logs().to_vec(),
1472 )
1473 }
1474
1475 let result1 = run_complex_scenario();
1476 let result2 = run_complex_scenario();
1477
1478 assert_eq!(result1.0, result2.0);
1479 assert_eq!(result1.1, result2.1);
1480 assert_eq!(result1.2, result2.2);
1481 }
1482
1483 #[test]
1484 fn model_unchanged_when_not_running() {
1485 let mut sim = ProgramSimulator::new(Counter {
1486 value: 5,
1487 initialized: false,
1488 });
1489 sim.init();
1490
1491 sim.send(CounterMsg::Quit);
1492
1493 let value_before = sim.model().value;
1494 sim.send(CounterMsg::Increment);
1495 sim.send(CounterMsg::BatchIncrement(10));
1496 let value_after = sim.model().value;
1497
1498 assert_eq!(value_before, value_after);
1499 }
1500
1501 #[test]
1502 fn init_produces_consistent_command_log() {
1503 struct InitModel {
1505 init_ran: bool,
1506 }
1507
1508 #[derive(Debug)]
1509 enum InitMsg {
1510 MarkInit,
1511 }
1512
1513 impl From<Event> for InitMsg {
1514 fn from(_: Event) -> Self {
1515 InitMsg::MarkInit
1516 }
1517 }
1518
1519 impl Model for InitModel {
1520 type Message = InitMsg;
1521
1522 fn init(&mut self) -> Cmd<Self::Message> {
1523 Cmd::msg(InitMsg::MarkInit)
1524 }
1525
1526 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1527 match msg {
1528 InitMsg::MarkInit => {
1529 self.init_ran = true;
1530 Cmd::none()
1531 }
1532 }
1533 }
1534
1535 fn view(&self, _frame: &mut Frame) {}
1536 }
1537
1538 let mut sim1 = ProgramSimulator::new(InitModel { init_ran: false });
1539 let mut sim2 = ProgramSimulator::new(InitModel { init_ran: false });
1540
1541 sim1.init();
1542 sim2.init();
1543
1544 assert_eq!(sim1.model().init_ran, sim2.model().init_ran);
1545 assert_eq!(sim1.command_log().len(), sim2.command_log().len());
1546 }
1547
1548 #[test]
1549 fn execute_cmd_directly() {
1550 let mut sim = ProgramSimulator::new(Counter {
1551 value: 0,
1552 initialized: false,
1553 });
1554
1555 sim.execute_cmd(Cmd::log("direct log"));
1557 sim.execute_cmd(Cmd::tick(std::time::Duration::from_secs(1)));
1558
1559 assert_eq!(sim.logs(), &["direct log"]);
1560 assert_eq!(sim.tick_rate(), Some(std::time::Duration::from_secs(1)));
1561 }
1562
1563 #[test]
1564 fn save_restore_are_noops_in_simulator() {
1565 let mut sim = ProgramSimulator::new(Counter {
1566 value: 7,
1567 initialized: false,
1568 });
1569 sim.init();
1570
1571 let log_len = sim.command_log().len();
1572 let tick_rate = sim.tick_rate();
1573 let value_before = sim.model().value;
1574
1575 sim.execute_cmd(Cmd::save_state());
1576 sim.execute_cmd(Cmd::restore_state());
1577
1578 assert_eq!(sim.command_log().len(), log_len);
1579 assert_eq!(sim.tick_rate(), tick_rate);
1580 assert_eq!(sim.model().value, value_before);
1581 assert!(sim.is_running());
1582 }
1583
1584 #[test]
1585 fn grapheme_pool_is_reused() {
1586 let mut sim = ProgramSimulator::new(Counter {
1587 value: 0,
1588 initialized: false,
1589 });
1590 sim.init();
1591
1592 for i in 0..10 {
1594 sim.model_mut().value = i;
1595 sim.capture_frame(80, 24);
1596 }
1597
1598 assert_eq!(sim.frame_count(), 10);
1599 }
1600
1601 #[test]
1612 fn contract_init_called_once_before_updates() {
1613 use std::sync::atomic::{AtomicUsize, Ordering as AO};
1614
1615 struct InitTracker {
1616 init_count: Arc<AtomicUsize>,
1617 update_count: Arc<AtomicUsize>,
1618 init_saw_zero_updates: bool,
1619 }
1620
1621 #[derive(Debug)]
1622 enum TrackerMsg {
1623 FromInit,
1624 Manual,
1625 }
1626
1627 impl From<Event> for TrackerMsg {
1628 fn from(_: Event) -> Self {
1629 TrackerMsg::Manual
1630 }
1631 }
1632
1633 impl Model for InitTracker {
1634 type Message = TrackerMsg;
1635
1636 fn init(&mut self) -> Cmd<Self::Message> {
1637 self.init_count.fetch_add(1, AO::SeqCst);
1638 self.init_saw_zero_updates = self.update_count.load(AO::SeqCst) == 0;
1639 Cmd::msg(TrackerMsg::FromInit)
1640 }
1641
1642 fn update(&mut self, _msg: Self::Message) -> Cmd<Self::Message> {
1643 self.update_count.fetch_add(1, AO::SeqCst);
1644 Cmd::none()
1645 }
1646
1647 fn view(&self, _frame: &mut Frame) {}
1648 }
1649
1650 let init_count = Arc::new(AtomicUsize::new(0));
1651 let update_count = Arc::new(AtomicUsize::new(0));
1652
1653 let mut sim = ProgramSimulator::new(InitTracker {
1654 init_count: init_count.clone(),
1655 update_count: update_count.clone(),
1656 init_saw_zero_updates: false,
1657 });
1658
1659 sim.init();
1660 assert_eq!(init_count.load(AO::SeqCst), 1, "init called exactly once");
1661 assert!(
1662 sim.model().init_saw_zero_updates,
1663 "init must run before any update"
1664 );
1665 assert_eq!(
1667 update_count.load(AO::SeqCst),
1668 1,
1669 "init's command should trigger update"
1670 );
1671 }
1672
1673 #[test]
1676 fn contract_on_shutdown_called_with_final_commands() {
1677 struct ShutdownTracker {
1678 shutdown_called: bool,
1679 final_log: Option<String>,
1680 }
1681
1682 #[derive(Debug)]
1683 enum ShutMsg {
1684 Quit,
1685 LogFinal(String),
1686 }
1687
1688 impl From<Event> for ShutMsg {
1689 fn from(_: Event) -> Self {
1690 ShutMsg::Quit
1691 }
1692 }
1693
1694 impl Model for ShutdownTracker {
1695 type Message = ShutMsg;
1696
1697 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1698 match msg {
1699 ShutMsg::Quit => Cmd::quit(),
1700 ShutMsg::LogFinal(s) => {
1701 self.final_log = Some(s);
1702 Cmd::none()
1703 }
1704 }
1705 }
1706
1707 fn view(&self, _frame: &mut Frame) {}
1708
1709 fn on_shutdown(&mut self) -> Cmd<Self::Message> {
1710 self.shutdown_called = true;
1711 Cmd::msg(ShutMsg::LogFinal("shutdown-complete".into()))
1712 }
1713 }
1714
1715 let mut sim = ProgramSimulator::new(ShutdownTracker {
1716 shutdown_called: false,
1717 final_log: None,
1718 });
1719 sim.init();
1720 sim.send(ShutMsg::Quit);
1721 sim.shutdown();
1722 sim.shutdown();
1723
1724 assert!(sim.model().shutdown_called, "on_shutdown must be called");
1725 assert!(sim.is_shutdown(), "shutdown state must be observable");
1726 assert_eq!(
1727 sim.model().final_log.as_deref(),
1728 Some("shutdown-complete"),
1729 "on_shutdown commands must be executed"
1730 );
1731 }
1732
1733 #[test]
1735 fn contract_batch_stops_on_quit() {
1736 struct BatchQuitModel {
1737 steps: Vec<&'static str>,
1738 }
1739
1740 #[derive(Debug)]
1741 enum BQMsg {
1742 Step(&'static str),
1743 TriggerBatchWithQuit,
1744 }
1745
1746 impl From<Event> for BQMsg {
1747 fn from(_: Event) -> Self {
1748 BQMsg::Step("event")
1749 }
1750 }
1751
1752 impl Model for BatchQuitModel {
1753 type Message = BQMsg;
1754
1755 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1756 match msg {
1757 BQMsg::Step(s) => {
1758 self.steps.push(s);
1759 Cmd::none()
1760 }
1761 BQMsg::TriggerBatchWithQuit => Cmd::batch(vec![
1762 Cmd::msg(BQMsg::Step("before-quit")),
1763 Cmd::quit(),
1764 Cmd::msg(BQMsg::Step("after-quit")),
1765 ]),
1766 }
1767 }
1768
1769 fn view(&self, _frame: &mut Frame) {}
1770 }
1771
1772 let mut sim = ProgramSimulator::new(BatchQuitModel { steps: vec![] });
1773 sim.init();
1774 sim.send(BQMsg::TriggerBatchWithQuit);
1775
1776 assert!(!sim.is_running(), "should be stopped");
1777 assert_eq!(
1778 sim.model().steps,
1779 vec!["before-quit"],
1780 "commands after Quit in a Batch must not execute"
1781 );
1782 }
1783
1784 #[test]
1786 fn contract_sequence_stops_on_quit() {
1787 struct SeqQuitModel {
1788 steps: Vec<&'static str>,
1789 }
1790
1791 #[derive(Debug)]
1792 enum SQMsg {
1793 Step(&'static str),
1794 TriggerSeqWithQuit,
1795 }
1796
1797 impl From<Event> for SQMsg {
1798 fn from(_: Event) -> Self {
1799 SQMsg::Step("event")
1800 }
1801 }
1802
1803 impl Model for SeqQuitModel {
1804 type Message = SQMsg;
1805
1806 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1807 match msg {
1808 SQMsg::Step(s) => {
1809 self.steps.push(s);
1810 Cmd::none()
1811 }
1812 SQMsg::TriggerSeqWithQuit => Cmd::sequence(vec![
1813 Cmd::msg(SQMsg::Step("before-quit")),
1814 Cmd::quit(),
1815 Cmd::msg(SQMsg::Step("after-quit")),
1816 ]),
1817 }
1818 }
1819
1820 fn view(&self, _frame: &mut Frame) {}
1821 }
1822
1823 let mut sim = ProgramSimulator::new(SeqQuitModel { steps: vec![] });
1824 sim.init();
1825 sim.send(SQMsg::TriggerSeqWithQuit);
1826
1827 assert!(!sim.is_running(), "should be stopped");
1828 assert_eq!(
1829 sim.model().steps,
1830 vec!["before-quit"],
1831 "commands after Quit in a Sequence must not execute"
1832 );
1833 }
1834
1835 #[test]
1838 fn contract_task_result_routes_through_update() {
1839 struct TaskModel {
1840 trace: Vec<String>,
1841 }
1842
1843 #[derive(Debug)]
1844 enum TMsg {
1845 Spawn,
1846 TaskDone(i32),
1847 }
1848
1849 impl From<Event> for TMsg {
1850 fn from(_: Event) -> Self {
1851 TMsg::Spawn
1852 }
1853 }
1854
1855 impl Model for TaskModel {
1856 type Message = TMsg;
1857
1858 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1859 match msg {
1860 TMsg::Spawn => {
1861 self.trace.push("spawn".into());
1862 Cmd::task(|| TMsg::TaskDone(42))
1863 }
1864 TMsg::TaskDone(v) => {
1865 self.trace.push(format!("done:{v}"));
1866 Cmd::none()
1867 }
1868 }
1869 }
1870
1871 fn view(&self, _frame: &mut Frame) {}
1872 }
1873
1874 let mut sim = ProgramSimulator::new(TaskModel { trace: vec![] });
1875 sim.init();
1876 sim.send(TMsg::Spawn);
1877
1878 assert_eq!(
1879 sim.model().trace,
1880 vec!["spawn", "done:42"],
1881 "task result must route through update()"
1882 );
1883 }
1884
1885 #[test]
1888 fn contract_cmd_msg_dispatches_recursively() {
1889 struct RecursiveModel {
1890 trace: Vec<i32>,
1891 }
1892
1893 #[derive(Debug)]
1894 enum RMsg {
1895 Chain(i32),
1896 }
1897
1898 impl From<Event> for RMsg {
1899 fn from(_: Event) -> Self {
1900 RMsg::Chain(0)
1901 }
1902 }
1903
1904 impl Model for RecursiveModel {
1905 type Message = RMsg;
1906
1907 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1908 match msg {
1909 RMsg::Chain(n) => {
1910 self.trace.push(n);
1911 if n < 3 {
1912 Cmd::msg(RMsg::Chain(n + 1))
1913 } else {
1914 Cmd::none()
1915 }
1916 }
1917 }
1918 }
1919
1920 fn view(&self, _frame: &mut Frame) {}
1921 }
1922
1923 let mut sim = ProgramSimulator::new(RecursiveModel { trace: vec![] });
1924 sim.init();
1925 sim.send(RMsg::Chain(0));
1926
1927 assert_eq!(
1928 sim.model().trace,
1929 vec![0, 1, 2, 3],
1930 "Cmd::Msg must dispatch recursively through update()"
1931 );
1932 }
1933
1934 #[test]
1937 fn contract_batch_normalization() {
1938 let empty: Cmd<CounterMsg> = Cmd::batch(vec![]);
1939 assert!(matches!(empty, Cmd::None), "empty batch must be Cmd::None");
1940
1941 let single: Cmd<CounterMsg> = Cmd::batch(vec![Cmd::quit()]);
1942 assert!(
1943 matches!(single, Cmd::Quit),
1944 "single-element batch must unwrap"
1945 );
1946
1947 let multi: Cmd<CounterMsg> = Cmd::batch(vec![Cmd::none(), Cmd::quit()]);
1948 assert!(
1949 matches!(multi, Cmd::Batch(_)),
1950 "multi-element batch stays Batch"
1951 );
1952 }
1953
1954 #[test]
1956 fn contract_sequence_normalization() {
1957 let empty: Cmd<CounterMsg> = Cmd::sequence(vec![]);
1958 assert!(
1959 matches!(empty, Cmd::None),
1960 "empty sequence must be Cmd::None"
1961 );
1962 }
1963
1964 #[test]
1967 fn contract_no_processing_after_quit() {
1968 let mut sim = ProgramSimulator::new(Counter {
1969 value: 0,
1970 initialized: false,
1971 });
1972 sim.init();
1973
1974 sim.send(CounterMsg::Increment); sim.send(CounterMsg::Quit);
1976 sim.send(CounterMsg::Increment); sim.send(CounterMsg::Increment); assert_eq!(sim.model().value, 1, "messages after Quit must be ignored");
1980 assert!(!sim.is_running());
1981
1982 sim.inject_events(&[key_event('+'), key_event('+')]);
1984 assert_eq!(
1985 sim.model().value,
1986 1,
1987 "events after Quit must also be ignored"
1988 );
1989 }
1990}