feat(player): FakePlayer and the conformance suite

DR-243. One set of behaviours every engine must satisfy, written before the
second engine exists so it cannot encode whatever the first happens to do —
which is how three playback implementations drifted apart in the first place.

FakePlayer models the one behaviour that matters most: opening is not
instantaneous. `open` parks in Phase::Opening until complete_open() is called,
so a test can put a seek into that window deliberately. That window is where
DR-241 lived, and it was previously unreachable from any test.

The suite drives readiness through a Harness rather than sleeping — the fake
completes on demand, a real engine waits for its own readiness event. A
timing-dependent suite is worse than none, because it teaches people to
re-run until green.

Nine cases, each naming the defect it prevents:

  opens_at_a_start_position          DR-241 - starts there, never at zero
  seek_while_opening_is_honoured     DR-241 - held, not discarded
  seek_while_opening_overrides_start         later intent wins
  pause_and_play_are_observable      DR-239 - state an engine cannot hide
  close_is_silent_and_idempotent             stopped must mean silent
  close_during_open_never_plays              an open cancelled by close
                                             must not come back to life

`audible()` may return None for engines that cannot answer, which skips the
silence assertions rather than passing them vacuously — an assertion that
cannot fail is worse than an absent one.

Also adds MediaItem::sample: the struct has twenty-odd fields, almost none of
which a given test cares about, and repeating the literal per test is how a
new field ends up added in thirty places.
This commit is contained in:
2026-08-22 21:18:10 +02:00
parent 3b91922cca
commit f4892f4cb2
5 changed files with 614 additions and 0 deletions
@@ -0,0 +1,56 @@
//! `FakePlayer` runs the conformance suite.
//!
//! It is correct by construction, so a failure here means the *suite* is wrong,
//! not an engine. That is what makes it safe to trust the same cases when they
//! fail against a real one.
//!
//! TRACES: UR-081 | DR-243 | UT-220
use std::time::Duration;
use super::conformance::Harness;
use super::fake_player::FakePlayer;
use super::media::MediaItem;
use super::media_player::OpenRequest;
use crate::repository::stream_selection::StreamSelection;
struct FakeHarness {
player: FakePlayer,
}
impl FakeHarness {
fn new() -> Self {
Self {
player: FakePlayer::new().with_duration(Duration::from_secs(7200)),
}
}
}
impl Harness for FakeHarness {
type Player = FakePlayer;
fn player(&mut self) -> &mut FakePlayer {
&mut self.player
}
fn request(&self, start: Duration) -> OpenRequest {
let selection = StreamSelection::local_file("http://example.invalid/stream.mp4");
let media = MediaItem::sample("fake-item", &selection.url);
OpenRequest::new(media, selection).starting_at(start)
}
fn settle(&mut self) {
self.player.complete_open();
}
fn audible(&mut self) -> Option<bool> {
Some(self.player.audible)
}
/// Exact: the fake has no keyframes to round to, so any drift is a bug.
fn seek_tolerance(&self) -> Duration {
Duration::ZERO
}
}
crate::media_player_conformance!(fake, FakeHarness::new());