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}