Skip to main content

retroglyph_core/testing/
recording.rs

1//! Serialized input sessions: [`InputRecording`], the type [`TestHarness::replay`](super::TestHarness::replay) and
2//! [`TestHarness::from_recording`](super::TestHarness::from_recording) drive.
3
4use crate::event::Event;
5use alloc::vec::Vec;
6use core::time::Duration;
7
8/// A recorded input session: the backend size it was captured against, plus a timed sequence of
9/// events.
10///
11/// Each event's [`Duration`] is the delay since the *previous* recorded event (or since the
12/// start of the recording, for the first one), not an absolute timestamp measured from session
13/// start -- see [`push`](Self::push). That's the shape [`TestHarness::replay`](super::TestHarness::replay) needs to
14/// reproduce recorded pacing faithfully instead of collapsing every gap to zero.
15///
16/// `no_std` + `alloc` compatible, gated on the `testing` feature like the rest of this module.
17/// [`Serialize`](serde::Serialize)/[`Deserialize`](serde::Deserialize) are additionally gated on
18/// `serde`: driving a recording back through a live `App` within the same process (what this
19/// type and [`TestHarness::replay`](super::TestHarness::replay) do) needs no serialization at all; turning a
20/// recording into bytes on disk (the `.rgrec` format) is `retroglyph-recorder`'s job, built on
21/// top of these derives.
22#[derive(Debug, Clone, PartialEq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub struct InputRecording {
25    width: u16,
26    height: u16,
27    events: Vec<(Duration, Event)>,
28}
29
30impl InputRecording {
31    /// Creates an empty recording for a `width` x `height` backend.
32    #[must_use]
33    pub const fn new(width: u16, height: u16) -> Self {
34        Self {
35            width,
36            height,
37            events: Vec::new(),
38        }
39    }
40
41    /// The backend width this recording was captured against.
42    #[must_use]
43    pub const fn width(&self) -> u16 {
44        self.width
45    }
46
47    /// The backend height this recording was captured against.
48    #[must_use]
49    pub const fn height(&self) -> u16 {
50        self.height
51    }
52
53    /// Appends `event`, `delay` after the previously appended event (or after the start of the
54    /// recording, for the first).
55    pub fn push(&mut self, delay: Duration, event: Event) {
56        self.events.push((delay, event));
57    }
58
59    /// Iterates the recorded `(delay, event)` pairs, in the order they were
60    /// [`push`](Self::push)ed.
61    #[must_use]
62    pub fn events(&self) -> impl ExactSizeIterator<Item = &(Duration, Event)> {
63        self.events.iter()
64    }
65
66    /// The number of recorded events.
67    #[must_use]
68    pub const fn len(&self) -> usize {
69        self.events.len()
70    }
71
72    /// Returns `true` if no events have been recorded.
73    #[must_use]
74    pub const fn is_empty(&self) -> bool {
75        self.events.is_empty()
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::event::{KeyCode, KeyEvent, KeyModifiers};
83
84    #[test]
85    fn new_recording_is_empty() {
86        let recording = InputRecording::new(80, 24);
87        assert_eq!(recording.width(), 80);
88        assert_eq!(recording.height(), 24);
89        assert!(recording.is_empty());
90        assert_eq!(recording.len(), 0);
91    }
92
93    #[test]
94    fn push_appends_in_order() {
95        let mut recording = InputRecording::new(4, 2);
96        let a = Event::Key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
97        let b = Event::Key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
98        recording.push(Duration::ZERO, a.clone());
99        recording.push(Duration::from_millis(50), b.clone());
100
101        let events: Vec<_> = recording.events().collect();
102        assert_eq!(
103            events,
104            vec![&(Duration::ZERO, a), &(Duration::from_millis(50), b)]
105        );
106        assert_eq!(recording.len(), 2);
107        assert!(!recording.is_empty());
108    }
109
110    #[cfg(feature = "serde")]
111    #[test]
112    fn round_trips_through_serde_json() {
113        let mut recording = InputRecording::new(80, 24);
114        recording.push(
115            Duration::ZERO,
116            Event::Key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE)),
117        );
118        recording.push(
119            Duration::from_millis(250),
120            Event::Key(KeyEvent::new(KeyCode::Enter, KeyModifiers::SHIFT)),
121        );
122
123        let json = serde_json::to_string(&recording).expect("serialize");
124        let round_tripped: InputRecording = serde_json::from_str(&json).expect("deserialize");
125        assert_eq!(round_tripped, recording);
126    }
127}