Skip to main content

shuyi_platform/
harness.rs

1//! A headless test driver for any game on the engine (game ADR-0007 layer 4):
2//! scripted key and controller-button presses and waits in, in frames of
3//! 1/60 s or by a tick stage's fixed steps ([`Harness::steps`]), screen
4//! names, snapshots and the audio asked for out. No window,
5//! no clock, no sound card: it runs the game in an [`App`] on the
6//! [`HeadlessPlatform`], through the same frame the devices run, and the
7//! headless backend's [`Speaker`](crate::headless::Speaker) says what music
8//! sounds, so a screen that keeps time with the music is tested too.
9//!
10//! A game wraps it with its own start-up and lookups (its flow, its
11//! battle); the scripts are the same for every game. With the `harness`
12//! feature.
13//!
14//! ```
15//! use shuyi_platform::harness::Harness;
16//! use shuyi_ui::toy;
17//!
18//! let mut h = Harness::new(toy::game());
19//! h.keys("f");
20//! assert_eq!(h.screens(), ["home", "menu"]);
21//! h.keys("f");
22//! assert_eq!(h.sounds(), ["click"]);
23//! ```
24//!
25//! [`Harness::new`] takes a game already running. [`Harness::start`] runs it
26//! from its real [`Start`], card and load first, as a device does;
27//! [`Harness::start_recording`] records that run ([`session`]),
28//! and [`Harness::from_session`] plays such a record to any frame
29//! ([`play_to`](Harness::play_to)).
30
31use crate::alloc_prelude::*;
32use alloc::{format, vec};
33use core::any::Any;
34use core::time::Duration;
35
36#[cfg(feature = "std")]
37use shuyi_assets::bundle::Layer;
38use shuyi_assets::keymap::{Button, Chord, CrankDef};
39use shuyi_assets::target::TargetProfile;
40use shuyi_ui::audio::{AudioRequest, MusicClock, MusicCommand};
41use shuyi_ui::input::crank::Crank;
42use shuyi_ui::input::frame_seconds;
43use shuyi_ui::input::pad::PadKind;
44use shuyi_ui::input::state::Device;
45use shuyi_ui::log::LogRing;
46use shuyi_ui::perf::{HeapCounts, HeapSnapshot, MemoryScopes, PerfClock, PerfRing};
47use shuyi_ui::storage::Storage;
48use shuyi_ui::{Ctx, Game, GameData, KeyPrompt, PlatformFacts, RawInputEvent};
49
50use crate::app::{App, Start, SystemHook};
51use crate::headless::{AddedPlay, HeadlessPlatform, PlayingSound, PresentHook, Presented};
52use crate::platform::{Millis, SystemEvent};
53use crate::script::{self, Planned};
54use crate::session::{self, Playback, Played, SessionError, SessionRecord, Stamps};
55
56#[cfg(feature = "std")]
57mod kept;
58#[cfg(feature = "std")]
59pub use kept::assert_session;
60
61/// Simulated length of one frame, in seconds (60 fps): a script's frame.
62pub use crate::script::FRAME_DT;
63
64/// Most frames one [`Harness::hold`] or [`Harness::wait`] may run (about
65/// half a simulated minute), so a runaway script fails instead of hanging.
66pub const MAX_FRAMES: u32 = 2_000;
67
68/// The longest wait of one step of a [`Script`](crate::script::Script) the
69/// Harness plays, in seconds: [`MAX_FRAMES`] frames.
70#[allow(clippy::cast_precision_loss)] // 2000 frames
71pub const MAX_WAIT: f32 = MAX_FRAMES as f32 * FRAME_DT;
72
73/// The clock of a Harness on a game's real [`Start`]: the whole milliseconds
74/// [`App::frame`] takes, from the microseconds its frames add up to, so
75/// frames of [`FRAME_DT`] come out as 16, 17 and 17 ms. All integers: the
76/// `no_std` build has no `f64::round`.
77#[derive(Debug, Clone, Copy)]
78struct FrameClock {
79    /// Where it started, whatever a record's clock had reached.
80    from: Millis,
81    /// The microseconds since.
82    micros: u64,
83}
84
85impl FrameClock {
86    fn at(from: Millis) -> Self {
87        Self { from, micros: 0 }
88    }
89
90    /// The time `seconds` later, which goes on from here.
91    fn after(&mut self, seconds: f32) -> Millis {
92        let elapsed = Duration::try_from_secs_f32(frame_seconds(seconds)).unwrap_or(Duration::ZERO);
93        let micros = elapsed.as_nanos().saturating_add(500) / 1000;
94        let micros = u64::try_from(micros).unwrap_or(u64::MAX);
95        self.micros = self.micros.saturating_add(micros);
96        Millis(self.from.0.wrapping_add(self.micros / 1000))
97    }
98}
99
100/// Drives a [`Game`] with simulated key presses and time, as the platform
101/// would: in an [`App`], on the [`HeadlessPlatform`].
102pub struct Harness<G: GameData> {
103    app: App<G>,
104    platform: HeadlessPlatform,
105    /// The clock of a game from its [`Start`], which runs on [`App::frame`];
106    /// `None` for a game already running, on [`App::frame_after`].
107    clock: Option<FrameClock>,
108    /// The record playing, until a script's frame takes over from it.
109    playback: Option<Playback>,
110    /// How far it has played.
111    played: Played,
112    /// Every frame's audio requests, oldest first.
113    audio: Vec<Vec<AudioRequest<G::Voice>>>,
114    /// Every music command of the run, in order.
115    music: Vec<MusicCommand>,
116    /// The kind of controller [`pad`](Self::pad) presses buttons on.
117    pad_kind: PadKind,
118    /// The crank [`crank`](Self::crank) winds, on the [`use_pad`] pad.
119    ///
120    /// [`use_pad`]: Self::use_pad
121    crank: Crank,
122    /// How many frames have run.
123    frames: u32,
124    /// The screens open before the first frame, then after each frame
125    /// that changed them: the frame, and the names, bottom first.
126    stacks: Vec<(u32, Vec<&'static str>)>,
127    /// The frame of each of `music`'s commands.
128    music_frames: Vec<u32>,
129    /// The frame the game asked to quit in, once it has.
130    quit_frame: Option<u32>,
131}
132
133impl<G: GameData> Harness<G> {
134    /// Drives `game` from where it is.
135    pub fn new(game: Game<G>) -> Self {
136        Self::on(game, HeadlessPlatform::new())
137    }
138
139    /// Drives `game` from where it is on `platform` (one with a system
140    /// keyboard, say). Its screens get only the actions they declare
141    /// ([`Game::guard_actions`]), so a screen reacting to one it didn't
142    /// declare fails its test ([`undeclared`](Self::undeclared) says which
143    /// were held back).
144    pub fn on(mut game: Game<G>, platform: HeadlessPlatform) -> Self {
145        game.guard_actions(true);
146        Self::driving(App::running(game), platform, None)
147    }
148
149    /// Runs the game from its real `start`, as a device runs it: the card's
150    /// frame at 0 ms and the load's one [`FRAME_DT`] later run now, and the
151    /// load's errors are the `Err`. Its frames go through [`App::frame`], in
152    /// whole milliseconds. As on a device, its screens get the actions they
153    /// didn't declare too ([`on`](Self::on) holds those back), and a game
154    /// waits for its first press unless its start says not
155    /// ([`Setup::key_prompt`](crate::Setup::key_prompt)). The scripts work as
156    /// on any Harness.
157    pub fn start(start: Start<G>) -> Result<Self, String> {
158        Self::started(App::new(start))
159    }
160
161    /// [`start`](Self::start), recording from the first frame:
162    /// [`session`](Self::session) gives the record so far.
163    ///
164    /// A record holds what the platform gives the game, so what a test
165    /// changes around the platform isn't in it, and plays differently or
166    /// not at all: [`act`](Self::act), [`ctx_mut`](Self::ctx_mut),
167    /// [`screen_mut`](Self::screen_mut). Press the keys instead. Nor does it
168    /// hold a [`with_system_hook`](Self::with_system_hook) hook, so give the
169    /// hook in the `Start`, to both runs ([`Start::on_system`]).
170    pub fn start_recording(start: Start<G>) -> Result<Self, String> {
171        let mut app = App::new(start);
172        // No frame has run, so it always turns on.
173        let _ = app.record();
174        Self::started(app)
175    }
176
177    /// The record so far, of a [`start_recording`](Self::start_recording).
178    pub fn session(&self) -> Option<&SessionRecord> {
179        self.app.session()
180    }
181
182    /// Makes ready to play `record` from the game's real `start`, on the
183    /// display, the facts and the store it recorded
184    /// ([`HeadlessPlatform::from_session`]). It plays nothing yet:
185    /// [`play_to`](Self::play_to) does. A record of another version is
186    /// refused, and of another build as `stamps` says
187    /// ([`session::prepare`]).
188    pub fn from_session(
189        start: Start<G>,
190        record: &SessionRecord,
191        stamps: Stamps,
192    ) -> Result<Self, SessionError> {
193        let (start, limits) = session::prepare(start, record, stamps)?;
194        let platform = HeadlessPlatform::from_session(record);
195        let mut harness = Self::driving(App::new(start), platform, None);
196        harness.playback = Some(Playback::new(record));
197        harness.played.of = u32::try_from(record.frames.len()).unwrap_or(u32::MAX);
198        harness.played.limits = limits;
199        Ok(harness)
200    }
201
202    /// Plays the record's frames up to `frame`, counted from 1, or to its
203    /// end, and stops at the first frame whose hash differs, leaving the game
204    /// on that frame; after that it plays nothing more. On a frame before the
205    /// load, [`app`](Self::app), [`screens`](Self::screens) and
206    /// [`screen_log`](Self::screen_log) can be read, while what needs the
207    /// game panics. A Harness with no record plays nothing.
208    ///
209    /// A script's frame ends the playing, wherever it has got to: the game
210    /// goes on with the script, its clock from the last frame played
211    /// ([`Playback::now`]) and its music from the headless
212    /// [`Speaker`](crate::headless::Speaker), and [`Played::limits`] says so.
213    pub fn play_to(&mut self, frame: u32) -> &Played {
214        while self.played.first_difference.is_none() && self.played.frames < frame {
215            let Some(playback) = &mut self.playback else {
216                break;
217            };
218            let Some(answer) = playback.next(&mut self.app, &mut self.platform) else {
219                break;
220            };
221            self.played.frames += 1;
222            self.played.first_difference = answer.err();
223            self.note_frame();
224        }
225        &self.played
226    }
227
228    /// [`play_to`](Self::play_to) the record's end.
229    pub fn play_all(&mut self) -> &Played {
230        self.play_to(u32::MAX)
231    }
232
233    /// A script's frame is about to run: a record playing stops here.
234    fn leave_record(&mut self) {
235        let Some(playback) = self.playback.take() else {
236            return;
237        };
238        self.clock = Some(FrameClock::at(playback.now()));
239        self.platform.end_session();
240        let Played { frames, of, .. } = self.played;
241        self.played.limits.push(format!(
242            "after frame {frames} the music is the headless speaker's and no system keyboard is there: a script took over from the record"
243        ));
244        if frames < of {
245            self.played.limits.push(format!(
246                "the script took over before the record's end: {} of its frames didn't play",
247                of - frames
248            ));
249        }
250    }
251
252    /// Runs the first two frames of a Harness on a game's real [`Start`].
253    fn started(app: App<G>) -> Result<Self, String> {
254        let clock = FrameClock::at(Millis(0));
255        let mut harness = Self::driving(app, HeadlessPlatform::new(), Some(clock));
256        let _ = harness.app.frame(&mut harness.platform, clock.from);
257        harness.note_frame();
258        harness.frame(&[], FRAME_DT);
259        match harness.app.errors() {
260            Some(errors) => Err(errors.to_owned()),
261            None => Ok(harness),
262        }
263    }
264
265    /// A Harness over `app`, which has run no frame.
266    fn driving(app: App<G>, platform: HeadlessPlatform, clock: Option<FrameClock>) -> Self {
267        let stacks = vec![(0, app.game().map_or_else(Vec::new, Game::screens))];
268        Self {
269            app,
270            platform,
271            clock,
272            playback: None,
273            played: Played::default(),
274            audio: Vec::new(),
275            music: Vec::new(),
276            pad_kind: PadKind::default(),
277            crank: Crank::new(CrankDef::default()),
278            frames: 0,
279            stacks,
280            music_frames: Vec::new(),
281            quit_frame: None,
282        }
283    }
284
285    /// Gives the game `hook` for the system's events
286    /// ([`system`](Self::system)).
287    #[must_use]
288    pub fn with_system_hook(mut self, hook: SystemHook<G>) -> Self {
289        self.app.set_system_hook(hook);
290        self
291    }
292
293    /// Waits for a key or button press, as a platform's first screen may
294    /// ([`KeyPrompt::Waiting`]), and redraws.
295    #[must_use]
296    pub fn at_prompt(mut self) -> Self {
297        let game = self.game_mut();
298        game.ctx_mut().key_prompt = KeyPrompt::Waiting;
299        game.redraw();
300        self
301    }
302
303    /// Runs the game as the target `profile` describes: its facts
304    /// ([`PlatformFacts::from_profile`]) go into the context
305    /// ([`Ctx::platform`]), and the screen redraws. Call it before the
306    /// screen under test opens: a screen may keep what it read as it
307    /// opened (the key bindings screen keeps the keyboard).
308    #[must_use]
309    pub fn as_target(mut self, profile: &TargetProfile) -> Self {
310        let game = self.game_mut();
311        game.ctx_mut().platform = PlatformFacts::from_profile(profile);
312        game.redraw();
313        self
314    }
315
316    /// Makes music take `seconds` to load: a track starts sounding, and
317    /// [`Ctx::music_clock`] starts counting, that long after the game
318    /// starts it (at once by default). Negative or NaN counts as 0.
319    pub fn music_load_delay(&mut self, seconds: f32) -> &mut Self {
320        self.platform
321            .speaker_mut()
322            .set_load_delay(f64::from(seconds));
323        self
324    }
325
326    /// Makes every voice clip last `seconds`: [`Ctx::voice_playing`] names
327    /// its line from the frame after it was asked for until that long has
328    /// passed. Until this is called a clip sounds until it is stopped or
329    /// replaced. Negative or NaN counts as 0.
330    pub fn voice_length(&mut self, seconds: f32) -> &mut Self {
331        self.platform
332            .speaker_mut()
333            .set_voice_length(f64::from(seconds));
334        self
335    }
336
337    /// Makes no music ever sound, as when the music files are missing:
338    /// [`Ctx::music_clock`] stays `None`.
339    pub fn without_music(&mut self) -> &mut Self {
340        self.platform.speaker_mut().set_silent();
341        self
342    }
343
344    /// Where the music is in its track, as the screens saw it in the last
345    /// frame ([`Ctx::music_clock`]).
346    pub fn music_clock(&self) -> Option<&MusicClock> {
347        self.ctx().music_clock.as_ref()
348    }
349
350    /// From now on [`pad`](Self::pad) and [`hold_pad`](Self::hold_pad)
351    /// press buttons on a controller of `kind` (a generic one until then).
352    /// The button names in scripts stay binding positions: on a Nintendo
353    /// pad `South` is its right face button.
354    pub fn use_pad(&mut self, kind: PadKind) -> &mut Self {
355        self.pad_kind = kind;
356        self
357    }
358
359    /// Changes the file at `path` in `layer` (one handed in at run time),
360    /// as a file changing on disk does: the next frame's modules hear of it
361    /// before the update ([`HeadlessPlatform::change_file`]).
362    #[cfg(feature = "std")]
363    pub fn change_file(&mut self, layer: Layer, path: &str, bytes: Vec<u8>) -> &mut Self {
364        self.platform.change_file(layer, path, bytes);
365        self
366    }
367
368    /// Presses and releases each whitespace-separated chord in turn, e.g.
369    /// `"Down Down f"` or `"Shift+Space Enter"`: a frame with the press,
370    /// then a frame with the release, each [`FRAME_DT`] long.
371    ///
372    /// # Panics
373    ///
374    /// On a chord [`Chord::parse`] rejects.
375    pub fn keys(&mut self, script: &str) -> &mut Self {
376        let frames =
377            script::key_frames(script).unwrap_or_else(|e| panic!("bad chord in test script: {e}"));
378        self.play_planned(&frames);
379        self
380    }
381
382    /// Types `text` one character per frame as the platform reports
383    /// typing: the key's press (Shift for capitals) with the character,
384    /// then its release. A character without a key of its own is typed
385    /// alone.
386    pub fn type_text(&mut self, text: &str) -> &mut Self {
387        for c in text.chars() {
388            let name = match c {
389                ' ' => "Space".to_owned(),
390                c if c.is_ascii_uppercase() => format!("Shift+{}", c.to_ascii_lowercase()),
391                c => c.to_string(),
392            };
393            let Ok(chord) = Chord::parse(&name) else {
394                self.frame(&[RawInputEvent::Text(c)], FRAME_DT);
395                continue;
396            };
397            self.frame(
398                &[RawInputEvent::Down(chord), RawInputEvent::Text(c)],
399                FRAME_DT,
400            );
401            self.frame(&[RawInputEvent::Up(chord.key)], FRAME_DT);
402        }
403        self
404    }
405
406    /// Holds `chord` for `seconds` of frames (so held keys repeat), then
407    /// releases it in one more frame.
408    ///
409    /// # Panics
410    ///
411    /// On a chord [`Chord::parse`] rejects.
412    pub fn hold(&mut self, chord: &str, seconds: f32) -> &mut Self {
413        let chord = parse(chord);
414        self.advance(&[RawInputEvent::Down(chord)], seconds);
415        self.frame(&[RawInputEvent::Up(chord.key)], FRAME_DT);
416        self
417    }
418
419    /// Presses and releases each whitespace-separated controller button in
420    /// turn, e.g. `"DpadDown South"`, like [`keys`](Self::keys). The names
421    /// are button positions, as in a keymap file. Buttons joined by `+`
422    /// (`"South+East"`, `"East+DpadRight"`) go down together in one frame,
423    /// in the order written, and up together in the next.
424    ///
425    /// # Panics
426    ///
427    /// On a button [`Button::parse`] rejects.
428    pub fn pad(&mut self, script: &str) -> &mut Self {
429        let frames = script::pad_frames(script, self.pad_kind)
430            .unwrap_or_else(|e| panic!("bad button in test script: {e}"));
431        self.play_planned(&frames);
432        self
433    }
434
435    /// The kind of controller [`pad`](Self::pad) presses buttons on.
436    pub(crate) fn pad_kind(&self) -> PadKind {
437        self.pad_kind
438    }
439
440    /// Plays `frames` of a script's plan, each [`FRAME_DT`] long.
441    ///
442    /// # Panics
443    ///
444    /// On an idle run of more than [`MAX_FRAMES`] frames.
445    pub(crate) fn play_planned(&mut self, frames: &[Planned]) {
446        for planned in frames {
447            match planned {
448                Planned::Events(events) => {
449                    self.frame(events, FRAME_DT);
450                }
451                Planned::Idle(n) => {
452                    assert!(
453                        *n <= MAX_FRAMES,
454                        "Harness: more than {MAX_FRAMES} frames in one wait"
455                    );
456                    for _ in 0..*n {
457                        self.frame(&[], FRAME_DT);
458                    }
459                }
460            }
461        }
462    }
463
464    /// Notches of `def` for [`crank`](Self::crank) (the game's keymap
465    /// file's `crank`; 45° until set).
466    pub fn set_crank(&mut self, def: CrankDef) -> &mut Self {
467        self.crank = Crank::new(def);
468        self
469    }
470
471    /// Winds the crank `degrees` in one frame of [`FRAME_DT`] (forwards is
472    /// positive, backwards negative), as the Playdate's front end reports
473    /// it: a press and a release of a crank button for each whole notch,
474    /// the rest of a notch kept for the next call (ticket 1035).
475    pub fn crank(&mut self, degrees: f32) -> &mut Self {
476        let events = RawInputEvent::from_crank(&mut self.crank, degrees, false, self.pad_kind);
477        self.frame(&events, FRAME_DT)
478    }
479
480    /// Hands the top screen `action` from outside the keys, as a platform's
481    /// own menu does ([`Game::act`]), then runs a frame of [`FRAME_DT`]:
482    /// the menu closes and the game goes on.
483    pub fn act(&mut self, action: G::Action) -> &mut Self {
484        self.game_mut().act(action);
485        self.frame(&[], FRAME_DT)
486    }
487
488    /// Turns debug tools on, and the debug view's channels of the
489    /// comma-separated ids `channels` (`"all"`: every channel, offered or
490    /// not; ADR-0085), and draws the frame again with them: no frame
491    /// passes. A script's [`Step::Debug`](crate::script::Step::Debug).
492    pub fn show_debug(&mut self, channels: &str) -> &mut Self {
493        let game = self.game_mut();
494        let ctx = game.ctx_mut();
495        ctx.debug_tools = true;
496        for id in channels
497            .split(',')
498            .map(str::trim)
499            .filter(|id| !id.is_empty())
500        {
501            if id == "all" {
502                ctx.debug_view.show_all();
503            } else {
504                ctx.debug_view.set(id, true);
505            }
506        }
507        game.redraw();
508        self
509    }
510
511    /// The system tells the game `event` (a suspend, a resume, a lost
512    /// focus), in a frame of [`FRAME_DT`] with no input.
513    pub fn system(&mut self, event: SystemEvent) -> &mut Self {
514        self.platform.push_system(event);
515        self.frame(&[], FRAME_DT)
516    }
517
518    /// Holds the controller button `button` for `seconds` of frames (so a
519    /// held direction repeats), then releases it in one more frame.
520    ///
521    /// # Panics
522    ///
523    /// On a button [`Button::parse`] rejects.
524    pub fn hold_pad(&mut self, button: &str, seconds: f32) -> &mut Self {
525        let button = parse_button(button);
526        self.advance(&[RawInputEvent::PadDown(button, self.pad_kind)], seconds);
527        self.frame(&[RawInputEvent::PadUp(button)], FRAME_DT);
528        self
529    }
530
531    /// Lets `seconds` pass with no input, in frames of at most [`FRAME_DT`].
532    pub fn wait(&mut self, seconds: f32) -> &mut Self {
533        self.advance(&[], seconds);
534        self
535    }
536
537    /// Runs frames with no input until exactly `count` fixed simulation
538    /// steps were due in them ([`App::ticks`]), so a tick stage steps its
539    /// rules `count` times: frames of [`FRAME_DT`], which run at most two
540    /// steps, and a half one for the last step, which runs at most one.
541    ///
542    /// # Panics
543    ///
544    /// If that takes more than [`MAX_FRAMES`] frames: the game runs no
545    /// steps (it is suspended, or hasn't loaded).
546    pub fn steps(&mut self, count: u32) -> &mut Self {
547        let mut left = count;
548        for _ in 0..MAX_FRAMES {
549            if left == 0 {
550                return self;
551            }
552            let dt = if left >= 2 { FRAME_DT } else { FRAME_DT / 2.0 };
553            self.frame(&[], dt);
554            left = left.saturating_sub(self.app.ticks());
555        }
556        if left == 0 {
557            return self;
558        }
559        panic!("Harness: {left} of {count} steps still due after {MAX_FRAMES} frames");
560    }
561
562    /// Runs frames totalling `seconds` (at least one frame); `first` goes
563    /// into the first frame.
564    ///
565    /// # Panics
566    ///
567    /// If that takes more than [`MAX_FRAMES`] frames.
568    fn advance(&mut self, first: &[RawInputEvent], seconds: f32) {
569        let mut left = if seconds.is_finite() {
570            seconds.max(0.0)
571        } else {
572            0.0
573        };
574        let mut events = first;
575        for _ in 0..MAX_FRAMES {
576            let dt = left.min(FRAME_DT);
577            self.frame(events, dt);
578            events = &[];
579            left -= dt;
580            if left <= 0.0 {
581                return;
582            }
583        }
584        panic!("Harness: more than {MAX_FRAMES} frames in one hold or wait");
585    }
586
587    /// Runs one frame of `dt` seconds with `events`, as a platform does:
588    /// the events go to the headless backend, which the [`App`] reads in
589    /// its frame (the music sounding, the update, the audio played, the
590    /// frame presented); then the audio is recorded. The scripts above
591    /// are made of these.
592    pub fn frame(&mut self, events: &[RawInputEvent], dt: f32) -> &mut Self {
593        self.leave_record();
594        self.platform.push_input(events);
595        let _ = match &mut self.clock {
596            Some(clock) => self.app.frame(&mut self.platform, clock.after(dt)),
597            None => self.app.frame_after(&mut self.platform, dt),
598        };
599        self.note_frame();
600        self
601    }
602
603    /// Notes the frame just run: its audio, whether it quit, and the screens
604    /// it left, none while the game hasn't loaded.
605    fn note_frame(&mut self) {
606        self.frames += 1;
607        self.audio.push(self.app.audio().to_vec());
608        let music = self.app.music();
609        self.music.extend_from_slice(music);
610        self.music_frames
611            .extend(core::iter::repeat_n(self.frames, music.len()));
612        if self.app.has_quit() && self.quit_frame.is_none() {
613            self.quit_frame = Some(self.frames);
614        }
615        let screens = self.screens();
616        if self.stacks.last().is_none_or(|(_, last)| *last != screens) {
617            self.stacks.push((self.frames, screens));
618        }
619    }
620
621    /// How many frames have run since the Harness started.
622    pub fn frame_count(&self) -> u32 {
623        self.frames
624    }
625
626    /// The screens open before the first frame (frame 0), then after each
627    /// frame that changed them: the frame, and their names, bottom first.
628    pub fn screen_log(&self) -> &[(u32, Vec<&'static str>)] {
629        &self.stacks
630    }
631
632    /// The frame the game asked to quit in, if it has (frames count from
633    /// 1).
634    pub fn quit_frame(&self) -> Option<u32> {
635        self.quit_frame
636    }
637
638    /// Every audio request since the last [`clear_audio`](Self::clear_audio),
639    /// in order, each with the frame that asked for it.
640    pub fn audio_log(&self) -> Vec<(u32, &AudioRequest<G::Voice>)> {
641        let len = u32::try_from(self.audio.len()).unwrap_or(u32::MAX);
642        let first = self.frames.saturating_sub(len) + 1;
643        (first..)
644            .zip(&self.audio)
645            .flat_map(|(frame, requests)| requests.iter().map(move |r| (frame, r)))
646            .collect()
647    }
648
649    /// Every music command since the last
650    /// [`clear_audio`](Self::clear_audio), in order, each with the frame
651    /// that gave it.
652    pub fn music_log(&self) -> Vec<(u32, &MusicCommand)> {
653        self.music_frames.iter().copied().zip(&self.music).collect()
654    }
655
656    /// Every audio request of the run so far, in order.
657    pub fn audio_requests(&self) -> Vec<AudioRequest<G::Voice>> {
658        self.audio.concat()
659    }
660
661    /// The sound cues (not music) played or started so far, in order.
662    pub fn sounds(&self) -> Vec<String> {
663        self.audio
664            .iter()
665            .flatten()
666            .filter_map(|r| match r {
667                AudioRequest::PlaySound { cue, .. } | AudioRequest::StartSound { cue, .. } => {
668                    Some(cue.clone())
669                }
670                _ => None,
671            })
672            .collect()
673    }
674
675    /// The sounds started with a handle that are still playing, as the
676    /// headless backend's [`Speaker`](crate::headless::Speaker) holds them,
677    /// in id order.
678    pub fn sounds_playing(&self) -> &[PlayingSound] {
679        self.platform.speaker().sounds_playing()
680    }
681
682    /// The cues of the sounds the game added at run time
683    /// ([`AudioQueue::add_sound`](shuyi_ui::audio::AudioQueue::add_sound))
684    /// and hasn't removed, as the headless backend's speaker holds them, in
685    /// order.
686    pub fn sounds_added(&self) -> Vec<&str> {
687        let added = self.platform.speaker().added_sounds();
688        added.keys().map(String::as_str).collect()
689    }
690
691    /// Every play and start of a sound the game added, in order, with the
692    /// samples it played, as the headless backend's speaker heard them.
693    pub fn added_plays(&self) -> &[AddedPlay] {
694        self.platform.speaker().added_plays()
695    }
696
697    /// The lines whose voice clip was asked for so far, in order, as line
698    /// ids.
699    pub fn voices(&self) -> Vec<String> {
700        self.audio
701            .iter()
702            .flatten()
703            .filter_map(|r| match r {
704                AudioRequest::PlayVoice { line, .. } => Some(line.to_string()),
705                _ => None,
706            })
707            .collect()
708    }
709
710    /// The audio requests of the last frame run (a key press runs two
711    /// frames, press and release; this is the release).
712    pub fn last_frame_audio(&self) -> &[AudioRequest<G::Voice>] {
713        self.audio.last().map_or(&[], Vec::as_slice)
714    }
715
716    /// The volumes the last frame told the platform to play at, 0–1: the
717    /// music's and the sounds' ([`AudioFrame`](crate::AudioFrame)).
718    pub fn volumes(&self) -> (f32, f32) {
719        let (music, sound, _) = self.platform.speaker().volumes();
720        (music, sound)
721    }
722
723    /// The voice volume the last frame told the platform to play at, 0–1
724    /// ([`AudioFrame`](crate::AudioFrame)).
725    pub fn voice_volume(&self) -> f32 {
726        self.platform.speaker().volumes().2
727    }
728
729    /// Whether the last frame told the platform to fill the screen.
730    pub fn fullscreen(&self) -> bool {
731        matches!(
732            self.platform.last_frame(),
733            Some(Presented::Game {
734                fullscreen: true,
735                ..
736            })
737        )
738    }
739
740    /// Every music command of the run so far, in order.
741    pub fn music_commands(&self) -> &[MusicCommand] {
742        &self.music
743    }
744
745    /// Forgets the audio recorded so far, so the next checks see only
746    /// what comes after.
747    pub fn clear_audio(&mut self) -> &mut Self {
748        self.audio.clear();
749        self.music.clear();
750        self.music_frames.clear();
751        self
752    }
753
754    /// Name of the top screen; empty once every screen has closed, and while
755    /// the game hasn't loaded.
756    pub fn top_screen(&self) -> &'static str {
757        self.app.game().and_then(Game::top_screen).unwrap_or("")
758    }
759
760    /// Names of the open screens, bottom first: none while the game hasn't
761    /// loaded.
762    pub fn screens(&self) -> Vec<&'static str> {
763        self.app.game().map_or_else(Vec::new, Game::screens)
764    }
765
766    /// The top-most screen of type `T` on the stack, if it opts in
767    /// ([`Game::screen`]).
768    pub fn screen<T: Any>(&self) -> Option<&T> {
769        self.game().screen()
770    }
771
772    /// [`screen`](Self::screen), mutable, to drive it with scripted
773    /// commands.
774    pub fn screen_mut<T: Any>(&mut self) -> Option<&mut T> {
775        self.game_mut().screen_mut()
776    }
777
778    /// The current frame in the snapshot format of
779    /// [`shuyi_ui::frame::snapshot`]. The second cell of a wide character
780    /// (Japanese, Chinese) prints as nothing, so a row of wide text reads
781    /// as it is drawn; its colour row still has a key a cell.
782    pub fn snapshot(&self) -> String {
783        let game = self.game();
784        game.buffer().to_snapshot(&game.ctx().palette)
785    }
786
787    /// The current frame as it would look had the player pressed `device`
788    /// last: the same screen with that device's key or button names. For
789    /// comparing play on a controller with the same play on the keyboard.
790    pub fn snapshot_as(&mut self, device: Device) -> String {
791        let game = self.game_mut();
792        let used = core::mem::replace(&mut game.ctx_mut().device, device);
793        game.redraw();
794        let snapshot = self.snapshot();
795        let game = self.game_mut();
796        game.ctx_mut().device = used;
797        game.redraw();
798        snapshot
799    }
800
801    /// Every action held back from a screen that doesn't declare it
802    /// ([`Screen::actions`](shuyi_ui::Screen::actions)), oldest first, with
803    /// the screen's name: why a screen didn't react to a key.
804    pub fn undeclared(&self) -> &[(&'static str, G::Action)] {
805        self.game().undeclared_actions()
806    }
807
808    /// Whether the game has asked to quit.
809    pub fn quit_requested(&self) -> bool {
810        self.game().quit_requested()
811    }
812
813    /// The game being driven.
814    pub fn game(&self) -> &Game<G> {
815        self.app
816            .game()
817            .unwrap_or_else(|| panic!("Harness: the game isn't running"))
818    }
819
820    /// The game being driven, mutable.
821    fn game_mut(&mut self) -> &mut Game<G> {
822        self.app
823            .game_mut()
824            .unwrap_or_else(|| panic!("Harness: the game isn't running"))
825    }
826
827    /// The [`App`] running the game.
828    pub fn app(&self) -> &App<G> {
829        &self.app
830    }
831
832    /// The [`App`] running the game, mutable, to take what it hands a
833    /// backend ([`App::take_session_save`]).
834    pub fn app_mut(&mut self) -> &mut App<G> {
835        &mut self.app
836    }
837
838    /// The game's log, kept by its [`App`]: every line from frame 0 (what
839    /// its context logged before the Harness started), each with the frame
840    /// it was given in.
841    pub fn log(&self) -> &LogRing {
842        self.app.log()
843    }
844
845    /// The frames' records, kept by its [`App`] (ADR-0046): one for each
846    /// frame measured, with a clock set ([`set_clock`](Self::set_clock))
847    /// and debug tools on.
848    pub fn perf(&self) -> &PerfRing {
849        self.app.perf()
850    }
851
852    /// Measures frames with `clock` from the next frame, or stops (`None`):
853    /// the headless backend's clock, and the game's context's, since the
854    /// game runs. A fake clock ([`PerfClock::fake`]) gives the same records
855    /// on every run.
856    pub fn set_clock(&mut self, clock: Option<PerfClock>) -> &mut Self {
857        self.platform.set_clock(clock);
858        self.ctx_mut().set_perf_clock(clock);
859        self
860    }
861
862    /// Tags what the game allocates with `scopes` from the next frame, or
863    /// stops (`None`): the headless backend's memory tags, and the game's
864    /// context's, since the game runs ([`MemoryScopes`]).
865    pub fn set_scopes(&mut self, scopes: Option<MemoryScopes>) -> &mut Self {
866        self.platform.set_scopes(scopes);
867        self.ctx_mut().set_memory_scopes(scopes);
868        self
869    }
870
871    /// Reads the heap with `memory` (a real counter's reading) at the end
872    /// of each frame measured from now on, or reads none (`None`): each
873    /// record's [`heap`](shuyi_ui::perf::FrameRecord::heap). A fake memory
874    /// that moves by fixed amounts is the platform's
875    /// ([`HeadlessPlatform::with_fake_memory`], with [`Harness::on`]).
876    pub fn set_memory(&mut self, memory: Option<fn() -> HeapCounts>) -> &mut Self {
877        self.platform.set_memory(memory);
878        self
879    }
880
881    /// Takes heap snapshots with `snapshot` (a real counter's) from now
882    /// on, or none (`None`). A fake memory takes its own
883    /// ([`HeadlessPlatform::with_fake_memory`]).
884    pub fn set_heap_snapshots(&mut self, snapshot: Option<fn() -> HeapSnapshot>) -> &mut Self {
885        self.platform.set_heap_snapshots(snapshot);
886        self
887    }
888
889    /// Runs `hook` on each frame presented from now on, in the headless
890    /// backend's present, or runs none (`None`): the work a device's
891    /// backend does on each frame, such as a one-bit display's conversion
892    /// (`to_one_bit`), so `shuyi bench` counts it in the frame
893    /// ([`HeadlessPlatform::set_present_hook`]).
894    pub fn set_present_hook(&mut self, hook: Option<PresentHook>) -> &mut Self {
895        self.platform.set_present_hook(hook);
896        self
897    }
898
899    /// What holds the heap now, by memory tag, stamped with the frames run
900    /// ([`App::heap_snapshot`]): a fake memory's, or a counter's set with
901    /// [`set_heap_snapshots`](Self::set_heap_snapshots); `None` without
902    /// either, or with debug tools off. A test takes two and compares them
903    /// ([`HeapSnapshot::diff`]).
904    pub fn heap_snapshot(&mut self) -> Option<HeapSnapshot> {
905        self.app.heap_snapshot(&mut self.platform)
906    }
907
908    /// The headless backend it runs on: the frames presented, the audio
909    /// played.
910    pub fn platform(&self) -> &HeadlessPlatform {
911        &self.platform
912    }
913
914    /// The shared context.
915    pub fn ctx(&self) -> &Ctx<G> {
916        self.game().ctx()
917    }
918
919    /// The shared context, to change it between key presses, e.g.
920    /// `h.ctx_mut().set_layout_bindings(…)` to rebind keys as a key
921    /// bindings screen would. The next frame uses the new keys.
922    pub fn ctx_mut(&mut self) -> &mut Ctx<G> {
923        self.game_mut().ctx_mut()
924    }
925
926    /// Ends the run, handing back its storage, to start the next launch
927    /// with it.
928    pub fn into_storage(self) -> Box<dyn Storage> {
929        let game = self
930            .app
931            .into_game()
932            .unwrap_or_else(|| panic!("Harness: the game isn't running"));
933        game.into_ctx().storage.into_computer()
934    }
935}
936
937/// Plays the session record in `text` from `start`, stamps checked, to its
938/// end ([`Harness::from_session`]). The error says why it can't be played,
939/// or names the first frame that played differently.
940pub fn check_session<G: GameData>(start: Start<G>, text: &str) -> Result<Played, String> {
941    let record = SessionRecord::from_ron(text).map_err(|e| e.to_string())?;
942    let mut harness =
943        Harness::from_session(start, &record, Stamps::Check).map_err(|e| e.to_string())?;
944    let played = harness.play_all().clone();
945    match played.first_difference {
946        Some(difference) => Err(format!(
947            "frame {} played differently: the record says {}, this build {}",
948            difference.frame, difference.recorded, difference.played
949        )),
950        None => Ok(played),
951    }
952}
953
954/// The session record in `text` made again from `start`: every frame played,
955/// differences and all, and recorded. The same inputs, this build's stamp
956/// and hashes. The error says why it can't be played, or that this build
957/// reads the platform differently, so its inputs are no longer the record's.
958pub fn bless_session<G: GameData>(start: Start<G>, text: &str) -> Result<String, String> {
959    let old = SessionRecord::from_ron(text).map_err(|e| e.to_string())?;
960    let (start, _) = session::prepare(start, &old, Stamps::Ignore).map_err(|e| e.to_string())?;
961    let mut platform = HeadlessPlatform::from_session(&old);
962    let mut app = App::new(start);
963    // No frame has run, so it always turns on.
964    let _ = app.record();
965    let mut playback = Playback::new(&old);
966    while playback.next(&mut app, &mut platform).is_some() {}
967    let mut new = app
968        .take_session()
969        .ok_or("the record stopped being recorded")?;
970    new.voice_manifest.clone_from(&old.voice_manifest);
971    let inputs = |record: &SessionRecord| -> Vec<Vec<RawInputEvent>> {
972        let frames = record.frames.iter();
973        frames.map(|frame| frame.input.clone()).collect()
974    };
975    if inputs(&new) != inputs(&old) {
976        return Err("this build reads the platform differently: record the session again".into());
977    }
978    Ok(new.to_ron())
979}
980
981fn parse(chord: &str) -> Chord {
982    match Chord::parse(chord) {
983        Ok(chord) => chord,
984        Err(e) => panic!("bad chord in test script: {e}"),
985    }
986}
987
988fn parse_button(button: &str) -> Button {
989    match Button::parse(button) {
990        Ok(button) => button,
991        Err(e) => panic!("bad button in test script: {e}"),
992    }
993}