From 6a6284c5d63f4e945dca2f9fa4c467d135326401 Mon Sep 17 00:00:00 2001 From: Nils Lehnen <30603423+iderex@users.noreply.github.com> Date: Thu, 3 Sep 2026 07:24:48 +0200 Subject: [PATCH] Make the report 0057 decides, and put every one of them through 0047's queue 0057 fixes that every position report goes onto the queue in 0047 rather than to a server, that five events report the moment they happen, and that the interval produces one while something is playing. The interval and the events were values in src/playback/cadence.rs and nothing made a report: no position was ever enqueued, and #57's three open conditions had no subject. src/playback/report.rs is the report. It takes the queue on every call that reports and hands back what the queue did, so a report that was not asked of the queue is not a value it can produce; it carries the position always, which is 0056's rule about a report with no position reading as the item finished; and it makes no report between interval ticks, which is the alternative 0057 refuses first. Four guards were watched failing. Deleting the question of whether a report is due reddens four cases, which is reporting on every position change. Leaving the interval where it was after an interval report reddens one. Leaving it where it was after an event reddens five, the paused case among them. Making a seek wait for the interval reddens three, and the scrub case is one of them. No coalescing rule is written here: forty seeks leave one entry because 0047 coalesces at enqueue, and src/server/write_queue.rs is untouched. Which request carries a report is not decided here. 0010 classifies the paths for playback started and ended as accumulations, and 0047 does not queue an accumulation, so how a report occasioned by started or stopped is delivered is a question between those two records and 0057 that #27's transport meets. Closes #57. Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com> --- src/lib.rs | 4 + src/playback/cadence.rs | 20 +- src/playback/mod.rs | 12 +- src/playback/report.rs | 686 +++++++++++++++++++++++++++++++++++++ tests/thread_statements.rs | 21 ++ 5 files changed, 731 insertions(+), 12 deletions(-) create mode 100644 src/playback/report.rs diff --git a/src/lib.rs b/src/lib.rs index 681c0fa..5e22fcd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -226,6 +226,10 @@ const _: () = { any_thread::(); any_thread::(); any_thread::(); + any_thread::(); + any_thread::(); + any_thread::(); + any_thread::(); any_thread::(); any_thread::(); any_thread::(); diff --git a/src/playback/cadence.rs b/src/playback/cadence.rs index 17d675a..98e21ff 100644 --- a/src/playback/cadence.rs +++ b/src/playback/cadence.rs @@ -14,12 +14,14 @@ //! paused, which five events do not wait, and which of them starts, stops or //! leaves the interval alone. //! -//! WHAT IS NOT HERE IS THE REPORT. Nothing in this tree plays anything, holds a -//! session or reaches a server, so no position is produced, nothing is enqueued -//! and nothing is sent. This module holds the interval such a thing would run -//! on. #57's three open conditions are a scrub producing one report, each -//! immediate event reporting without waiting, and every report observed passing -//! through the queue, and none of them is met by anything here. +//! WHAT IS NOT HERE IS THE REPORT, AND IT IS BESIDE THIS FILE RATHER THAN +//! ABSENT. [`super::report`] is the one act that puts a position on the queue +//! in 0047, on each of the five events and when this interval says one is due, +//! and its cases are where #57's three conditions are asked: a scrub producing +//! one report, each immediate event reporting without waiting, and every report +//! observed passing through the queue. Nothing in this tree plays anything or +//! reaches a server, so what is proven there is the reporting and never the +//! delivery. This module holds the interval the report runs on. //! //! WHAT IS ALSO NOT HERE IS A SECOND COALESCING RULE, and its absence is the //! decision rather than an omission. #57's own condition that a scrub produces @@ -262,9 +264,9 @@ mod tests { //! 0057's interval, its five events and the constraint it puts on 0058, //! asked of the values. //! - //! What these cannot ask is any of #57's three open conditions. Each is - //! about a report being made and observed on a queue, and nothing in this - //! tree plays anything. + //! What these do not ask is any of #57's three conditions. Each is about a + //! report being made and observed on a queue, and the cases in + //! [`crate::playback::report`] are where they are asked. use super::{ A_POSITION_IS_REPORTED_EVERY, ReportsWithoutWaiting, TheInterval, WhatItDoesToTheInterval, diff --git a/src/playback/mod.rs b/src/playback/mod.rs index d521848..19755fd 100644 --- a/src/playback/mod.rs +++ b/src/playback/mod.rs @@ -22,7 +22,9 @@ //! 0060's completion rule, which takes [`resume`]'s boundary rather than //! stating a second one. [`cadence`] carries 0057's interval, the five events //! that do not wait for it, what each does to it, and the constraint 0057 puts -//! on [`resume`]'s rewind. +//! on [`resume`]'s rewind. [`report`] is the report itself: the one act that +//! puts a position on the queue in 0047, on each of those five events and when +//! the interval says one is due, and the place #57's three conditions are asked. //! //! # Why the type is named for the unit //! @@ -39,8 +41,11 @@ //! caller or from a server, and never at each use. [`AdmittedPosition::of`] is //! that act. THE SITES THAT CALL IT DO NOT EXIST IN THIS TREE. Nothing here //! reaches a server and nothing here holds an item, so no value enters the core -//! today and this module is the rule waiting for its callers: the reads are #39, -//! the reports are #57, and the handover is #111. +//! today and this module is the rule waiting for its callers: the reads are #39 +//! and the handover is #111. The reports are [`report`], and it is not one of +//! those sites on purpose: it takes an [`AdmittedPosition`] rather than a +//! number, so the act happens at whichever boundary hands it one, which is the +//! client-facing call #115's creation owes rather than anything here. //! //! # What this module does not report //! @@ -52,6 +57,7 @@ //! sentence. pub mod cadence; +pub mod report; pub mod resume; pub mod watched; diff --git a/src/playback/report.rs b/src/playback/report.rs new file mode 100644 index 0000000..a28e46a --- /dev/null +++ b/src/playback/report.rs @@ -0,0 +1,686 @@ +//! The report: the one act that puts a playback position on the queue. +//! +//! `docs/decisions/0057-the-progress-reporting-cadence.md` is the record and +//! #57 is the issue. [`super::cadence`] holds what that record decides about +//! WHEN a report is made; this module is the report itself, which is the part +//! #57's three conditions are about: a scrub producing one report, each of the +//! five events reporting without waiting for the interval, and every report +//! observed passing through the queue in 0047. +//! +//! # There is one route and it is the queue +//! +//! 0057 says every report is put on the queue in 0047 rather than sent, and +//! 0047 says why in its first section: a path that sends directly and queues +//! only when the server is away is two paths that agree until they do not, and +//! the disagreement is reachable only on a device whose connectivity changed +//! mid-playback, which is the phone on the train the queue was built for. So +//! every call below that makes a report takes the queue as a parameter and +//! hands back what the queue answered. There is no second way out of this +//! module, and the type is what holds that rather than a sentence: a report is +//! [`WhatTheEnqueueDid`], and nothing here can produce one without asking the +//! queue. +//! +//! # A report is an assertion, and it always states the position +//! +//! 0047 queues assertions of a desired state and never deltas, so that a +//! delivery repeated after a flaky reconnection has no second effect. A +//! [`PositionReport`] is one: where playback of this item is, now. 0056 adds +//! that the core always states the position on every report, because the server +//! reads an absent position as the whole duration, which is the item finished. +//! The type has no absent value for that reason, and a position at the +//! beginning is a position rather than an absence. +//! +//! # Coalescing is not here, and that is 0057's own instruction +//! +//! #57 asks that a person scrubbing through a film sends one report rather than +//! forty. That is true here because 0047 coalesces at the moment of enqueue, +//! per target and per kind, and 0057 says writing a second rule for it is the +//! thing to avoid. What this module adds is only what that record adds: the +//! target is the item, the kind is the position, and every report of either +//! occasion below is the same kind, which is why forty seeks leave one entry. +//! +//! # What is here, and what is deliberately not +//! +//! What is here is the reporting: on which occasions a report is made, what it +//! carries, and that it goes through the queue and nowhere else. +//! +//! WHAT IS NOT HERE IS THE DELIVERY. Nothing in this tree opens a connection, +//! so no report leaves the device and no drain runs. The queue is 0047's, the +//! drain runs on 0045's recovery report, and which request carries a report is +//! #10's table met by #27's transport. 0057's honest statement about the report +//! a person most expects to have landed applies to every report here: durable +//! rather than delivered, and today the queue is not durable either, which +//! [`crate::server::write_queue`] says of itself. +//! +//! WHAT IS ALSO NOT HERE IS THE PLAYER. 0003 puts decoding and presenting +//! outside the core, so the core never sees a stream and never learns a position +//! by itself. A client tells it: the five events as they happen, and the +//! position as often as the platform's player reports one. What the core decides +//! is whether that observation becomes a report, which is the interval's +//! question and is answered by [`super::cadence`] rather than by the caller. +//! +//! WHAT IS ALSO NOT HERE IS THE ADMISSION. 0056 applies its two bounds where a +//! value enters the core, and [`Reporting`] takes an [`AdmittedPosition`] +//! rather than a number, so a position that reaches this module has already been +//! admitted and cannot have arrived any other way. + +use super::AdmittedPosition; +use super::Ticks; +use super::cadence::{ReportsWithoutWaiting, TheInterval}; +use crate::clock::ElapsedInstant; +use crate::server::write_queue::{Target, WhatIsAsserted, WhatTheEnqueueDid, WriteQueue}; + +/// What occasioned a report. +/// +/// Two occasions and no third. 0057 fixes that a report is made on each of the +/// five events the moment it happens, and on the interval while something is +/// playing, and it names nothing else that produces one. +/// +/// It is carried on the report rather than discarded so that whatever delivers +/// the report can tell a stop from a tick, which #10's table separates by path. +/// It does not change what the report asserts, which is the position, and it +/// does not change how the queue coalesces, which is per target and per kind. +/// +/// Thread safety, from 0009: a plain value, safe from any thread. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReportedOn { + /// One of the five events that report without waiting for the interval. + Event(ReportsWithoutWaiting), + /// The interval said a report was due. + TheInterval, +} + +/// What one report asserts: where playback of one item is. +/// +/// This is the assertion 0047 requires of everything it queues, and it is the +/// value a later report for the same item replaces. Forty seeks in five seconds +/// produce forty of these and leave one on the queue, holding the last +/// position rather than the first, which 0057 states as the consequence worth +/// stating. +/// +/// The position is always present, for 0056's reason: a report carrying no +/// position is read by the server as the item finished. +/// +/// Thread safety, from 0009: a plain value, safe from any thread. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PositionReport { + position: Ticks, + reported_on: ReportedOn, +} + +impl PositionReport { + /// Where playback is, in 0056's unit. + #[must_use] + pub const fn position(self) -> Ticks { + self.position + } + + /// What occasioned this report. + #[must_use] + pub const fn reported_on(self) -> ReportedOn { + self.reported_on + } +} + +/// What observing a position did. +/// +/// Three answers and never nothing, so a caller that hands the core every +/// position the player produces can see which of them became a report, and +/// a case can assert that the ones between reports did not. +/// +/// Thread safety, from 0009: a plain value, safe from any thread. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum WhatObservingDid { + /// The interval said a report was due, one was made, and this is what the + /// queue did with it. + Reported(WhatTheEnqueueDid), + /// Playback is running and the next report is not due yet. The position was + /// read and nothing was enqueued, which is the difference between this + /// module and reporting on every position change, the alternative 0057 + /// prices at a request per second per stream. + NotDueYet, + /// Nothing is playing: the item is paused, stopped or not yet started, and + /// 0057 fixes that nothing is ever due then, however long it lasts. + NothingIsPlaying, +} + +/// The reporting for one item within one session. +/// +/// It holds the item a report is about and where 0057's interval stands for +/// it, and nothing else. It holds no position, because the position is the +/// player's and arrives with every call; it holds no queue, because the queue is +/// the session's and outlives any one item's playback, which is why every call +/// that reports takes the queue rather than owning one. +/// +/// Thread safety, from 0009: a plain value, safe from any thread. It has no +/// interior mutability, so a caller sharing one across threads gives it the +/// same treatment as any other. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Reporting { + target: Target, + interval: TheInterval, +} + +impl Reporting { + /// The reporting for an item nothing is playing yet. + /// + /// The interval starts stopped, so a position observed before the started + /// event answers [`WhatObservingDid::NothingIsPlaying`] rather than becoming + /// a report about something that is not happening. What starts it is + /// [`Reporting::report`] with [`ReportsWithoutWaiting::Started`], which is + /// itself the first report. + #[must_use] + pub const fn for_item(target: Target, at: ElapsedInstant) -> Self { + Self { + target, + interval: TheInterval::stopped_at(at), + } + } + + /// The item this reporting is about. + #[must_use] + pub const fn target(&self) -> &Target { + &self.target + } + + /// Where 0057's interval stands for this item. + #[must_use] + pub const fn interval(&self) -> TheInterval { + self.interval + } + + /// One of the five events happened: report it now, and move the interval + /// the way that event moves it. + /// + /// The report is made before the interval is consulted and whatever the + /// interval says, which is 0057's sentence that the five events report the + /// moment they happen. A seek one second after the last tick reports; a + /// pause nine seconds in reports; and the interval is then left where + /// [`TheInterval::after`] puts it, which for a seek is where it was. + /// + /// What comes back is what the queue did, and it is the only thing that + /// comes back: a report that was not asked of the queue does not exist. + pub fn report( + &mut self, + event: ReportsWithoutWaiting, + position: AdmittedPosition, + at: ElapsedInstant, + queue: &mut WriteQueue, + ) -> WhatTheEnqueueDid { + let what_the_queue_did = queue.enqueue( + self.target.clone(), + WhatIsAsserted::PlaybackPosition, + PositionReport { + position: position.position(), + reported_on: ReportedOn::Event(event), + }, + ); + self.interval = self.interval.after(event, at); + what_the_queue_did + } + + /// The player reported where it is: make a report if the interval says one + /// is due, and otherwise make none. + /// + /// This is the call a client makes as often as its player produces a + /// position, and the whole of 0057's cadence is that most of those calls + /// enqueue nothing. Reporting on every one of them is the first alternative + /// that record refuses, and it is the shape this method has the moment the + /// question below is deleted. + /// + /// A report made here moves the interval on from `now`, so the next one is + /// due ten seconds after this one rather than ten seconds after the last + /// event. + pub fn observe( + &mut self, + position: AdmittedPosition, + now: ElapsedInstant, + queue: &mut WriteQueue, + ) -> WhatObservingDid { + if !self.interval.is_running() { + return WhatObservingDid::NothingIsPlaying; + } + if !self.interval.a_report_is_due(now) { + return WhatObservingDid::NotDueYet; + } + let what_the_queue_did = queue.enqueue( + self.target.clone(), + WhatIsAsserted::PlaybackPosition, + PositionReport { + position: position.position(), + reported_on: ReportedOn::TheInterval, + }, + ); + self.interval = self.interval.after_a_report_at(now); + WhatObservingDid::Reported(what_the_queue_did) + } +} + +#[cfg(test)] +mod tests { + //! #57's three conditions, asked of the report and the queue together. + //! + //! A scrub producing one report, each of the five events reporting without + //! waiting for the interval, and every report observed passing through the + //! queue. What these cannot ask is delivery: nothing here sends a byte, and + //! the queue that holds every report is not durable. + + use super::{PositionReport, ReportedOn, Reporting, WhatObservingDid}; + use crate::clock::ElapsedInstant; + use crate::playback::cadence::ReportsWithoutWaiting; + use crate::playback::{AdmittedPosition, Ticks}; + use crate::server::write_queue::{ + Entry, Target, WhatIsAsserted, WhatTheEnqueueDid, WriteQueue, + }; + + const NANOS_IN_A_SECOND: u64 = 1_000_000_000; + + fn at(seconds: u64) -> ElapsedInstant { + ElapsedInstant::from_nanos(seconds * NANOS_IN_A_SECOND) + } + + fn played_to(seconds: i64) -> AdmittedPosition { + AdmittedPosition::of(Ticks::from_seconds(seconds).as_ticks(), None) + } + + fn item(identifier: &str) -> Target { + Target::item(identifier.to_string()) + } + + /// An item started at second zero, with its one report already on the + /// queue. + fn playing(identifier: &str, queue: &mut WriteQueue) -> Reporting { + let mut reporting = Reporting::for_item(item(identifier), at(0)); + let did = reporting.report(ReportsWithoutWaiting::Started, played_to(0), at(0), queue); + assert_eq!(did, WhatTheEnqueueDid::Added); + reporting + } + + /// The one entry the queue holds for an item, which is the last thing said + /// about it. + fn the_entry_for(identifier: &str, queue: &WriteQueue) -> PositionReport { + let entries: Vec<&PositionReport> = queue + .entries() + .iter() + .filter(|entry| entry.target() == &item(identifier)) + .map(Entry::assertion) + .collect(); + assert_eq!( + entries.len(), + 1, + "the queue holds {} entries for one item", + entries.len() + ); + *entries[0] + } + + /// #57's first condition. Forty seeks in five seconds are forty reports and + /// one entry, holding the last position rather than the first, which is + /// 0047's coalescing arriving through this module rather than a rule of its + /// own. + #[test] + fn a_scrub_produces_one_report() { + let mut queue = WriteQueue::empty(); + let mut reporting = playing("the-film", &mut queue); + + let mut answers = Vec::new(); + for scrub in 1..=40_i64 { + let position = played_to(scrub * 60); + let moment = at(u64::try_from(scrub).expect("forty is a small number") / 8); + answers.push(reporting.report( + ReportsWithoutWaiting::Seeked, + position, + moment, + &mut queue, + )); + } + + assert!( + answers + .iter() + .all(|answer| *answer == WhatTheEnqueueDid::ReplacedInPlace), + "a seek did something other than replace the entry in place: {answers:?}" + ); + assert_eq!(queue.len(), 1); + assert_eq!(queue.dropped(), 0); + let entry = the_entry_for("the-film", &queue); + assert_eq!(entry.position(), Ticks::from_seconds(40 * 60)); + assert_eq!( + entry.reported_on(), + ReportedOn::Event(ReportsWithoutWaiting::Seeked) + ); + } + + /// #57's second condition, over the whole set of five rather than the + /// members somebody remembered. Each is reported one second into an + /// interval that is not due, and an observation at the same moment shows + /// the interval was indeed not due. + #[test] + fn each_immediate_event_reports_without_waiting_for_the_interval() { + for event in ReportsWithoutWaiting::all() { + let mut queue = WriteQueue::empty(); + let mut reporting = playing("the-film", &mut queue); + let before = reporting.interval(); + assert!( + !before.a_report_is_due(at(1)), + "the interval was already due, so this case proves nothing" + ); + + let did = reporting.report(*event, played_to(1), at(1), &mut queue); + + assert_eq!( + did, + WhatTheEnqueueDid::ReplacedInPlace, + "{} did not reach the queue", + event.as_str() + ); + let entry = the_entry_for("the-film", &queue); + assert_eq!(entry.reported_on(), ReportedOn::Event(*event)); + assert_eq!(entry.position(), Ticks::from_seconds(1)); + } + } + + /// The other half of the second condition: the interval is not what makes + /// an event report, so an observation at the same moment an event would + /// have reported makes none. This is the case that goes red when the + /// question in [`Reporting::observe`] is deleted, which is 0057's first + /// refused alternative. + #[test] + fn a_position_observed_before_the_interval_is_due_is_not_reported() { + let mut queue = WriteQueue::empty(); + let mut reporting = playing("the-film", &mut queue); + + for second in 1..=9_i64 { + let did = reporting.observe( + played_to(second), + at(u64::try_from(second).expect("nine is a small number")), + &mut queue, + ); + assert_eq!( + did, + WhatObservingDid::NotDueYet, + "a position {second} second(s) in became a report" + ); + } + + assert_eq!(queue.len(), 1); + assert_eq!( + the_entry_for("the-film", &queue).reported_on(), + ReportedOn::Event(ReportsWithoutWaiting::Started) + ); + } + + /// #57's third condition. Every report a whole viewing produces comes back + /// as what the queue did with it, and the queue afterwards holds exactly + /// what coalescing those reports leaves. There is no other route: a report + /// this module made and the queue did not see is not a value this module + /// can produce. + #[test] + fn every_report_passes_through_the_queue() { + let mut queue = WriteQueue::empty(); + let mut reporting = playing("the-film", &mut queue); + let mut reports = vec![WhatTheEnqueueDid::Added]; + + let observed = |reporting: &mut Reporting, + queue: &mut WriteQueue, + second: i64, + reports: &mut Vec| { + match reporting.observe( + played_to(second), + at(u64::try_from(second).expect("a viewing fits in a day")), + queue, + ) { + WhatObservingDid::Reported(did) => reports.push(did), + WhatObservingDid::NotDueYet | WhatObservingDid::NothingIsPlaying => {} + } + }; + + observed(&mut reporting, &mut queue, 5, &mut reports); + observed(&mut reporting, &mut queue, 10, &mut reports); + observed(&mut reporting, &mut queue, 20, &mut reports); + reports.push(reporting.report( + ReportsWithoutWaiting::Paused, + played_to(25), + at(25), + &mut queue, + )); + observed(&mut reporting, &mut queue, 3600, &mut reports); + reports.push(reporting.report( + ReportsWithoutWaiting::Resumed, + played_to(25), + at(3600), + &mut queue, + )); + reports.push(reporting.report( + ReportsWithoutWaiting::Seeked, + played_to(30), + at(3602), + &mut queue, + )); + observed(&mut reporting, &mut queue, 3610, &mut reports); + reports.push(reporting.report( + ReportsWithoutWaiting::Stopped, + played_to(45), + at(3620), + &mut queue, + )); + + assert_eq!( + reports.len(), + 8, + "the viewing made {} report(s)", + reports.len() + ); + assert_eq!(reports[0], WhatTheEnqueueDid::Added); + assert!( + reports[1..] + .iter() + .all(|did| *did == WhatTheEnqueueDid::ReplacedInPlace) + ); + assert_eq!(queue.len(), 1); + assert_eq!(queue.dropped(), 0); + assert_eq!( + queue.entries()[0].asserted_about(), + WhatIsAsserted::PlaybackPosition + ); + let entry = the_entry_for("the-film", &queue); + assert_eq!(entry.position(), Ticks::from_seconds(45)); + assert_eq!( + entry.reported_on(), + ReportedOn::Event(ReportsWithoutWaiting::Stopped) + ); + } + + /// Two items are two entries, which is 0057's target being the item within + /// the session: the person who started something else and came back has + /// told the server about two things. + #[test] + fn reports_for_two_items_do_not_collapse_into_one() { + let mut queue = WriteQueue::empty(); + let mut first = playing("the-film", &mut queue); + let mut second = Reporting::for_item(item("the-episode"), at(0)); + + let did = second.report( + ReportsWithoutWaiting::Started, + played_to(0), + at(1), + &mut queue, + ); + assert_eq!(did, WhatTheEnqueueDid::Added); + first.report( + ReportsWithoutWaiting::Seeked, + played_to(90), + at(2), + &mut queue, + ); + second.report( + ReportsWithoutWaiting::Seeked, + played_to(15), + at(3), + &mut queue, + ); + + assert_eq!(queue.len(), 2); + assert_eq!( + the_entry_for("the-film", &queue).position(), + Ticks::from_seconds(90) + ); + assert_eq!( + the_entry_for("the-episode", &queue).position(), + Ticks::from_seconds(15) + ); + } + + /// The interval, driven through the report rather than read on its own: + /// due ten seconds after the last report and not nine, and a report made + /// here moves it on. Deleting the move leaves the next report due at once. + #[test] + fn an_interval_report_is_made_ten_seconds_after_the_last_and_not_before() { + let mut queue = WriteQueue::empty(); + let mut reporting = playing("the-film", &mut queue); + + assert_eq!( + reporting.observe(played_to(9), at(9), &mut queue), + WhatObservingDid::NotDueYet + ); + assert_eq!( + reporting.observe(played_to(10), at(10), &mut queue), + WhatObservingDid::Reported(WhatTheEnqueueDid::ReplacedInPlace) + ); + assert_eq!( + the_entry_for("the-film", &queue).reported_on(), + ReportedOn::TheInterval + ); + assert_eq!( + reporting.observe(played_to(19), at(19), &mut queue), + WhatObservingDid::NotDueYet + ); + assert_eq!( + reporting.observe(played_to(20), at(20), &mut queue), + WhatObservingDid::Reported(WhatTheEnqueueDid::ReplacedInPlace) + ); + assert_eq!( + the_entry_for("the-film", &queue).position(), + Ticks::from_seconds(20) + ); + } + + /// A seek reports and leaves the interval alone, so a person scrubbing + /// steadily does not push the next interval report away. + #[test] + fn a_seek_reports_and_does_not_move_the_interval() { + let mut queue = WriteQueue::empty(); + let mut reporting = playing("the-film", &mut queue); + + for second in 1..=9_i64 { + reporting.report( + ReportsWithoutWaiting::Seeked, + played_to(second * 100), + at(u64::try_from(second).expect("nine is a small number")), + &mut queue, + ); + } + + assert_eq!( + reporting.observe(played_to(901), at(10), &mut queue), + WhatObservingDid::Reported(WhatTheEnqueueDid::ReplacedInPlace) + ); + } + + /// Nothing is reported while paused however long the pause is, which is + /// the wake-up per interval overnight that 0057 wrote the rule against. + /// The event itself reported, and that is the one entry. + #[test] + fn nothing_is_reported_while_paused_however_long_the_pause_is() { + let mut queue = WriteQueue::empty(); + let mut reporting = playing("the-film", &mut queue); + reporting.report( + ReportsWithoutWaiting::Paused, + played_to(4), + at(4), + &mut queue, + ); + + for now in [4_u64, 5, 14, 3600, 86_400] { + assert_eq!( + reporting.observe(played_to(4), at(now), &mut queue), + WhatObservingDid::NothingIsPlaying, + "a report was made {now} second(s) in while paused" + ); + } + + assert_eq!(queue.len(), 1); + assert_eq!( + the_entry_for("the-film", &queue).reported_on(), + ReportedOn::Event(ReportsWithoutWaiting::Paused) + ); + } + + /// Resuming reports, and the interval runs from the resume rather than from + /// the pause, so the time spent paused is not counted towards the next + /// report. + #[test] + fn resuming_reports_and_runs_the_interval_from_the_resume() { + let mut queue = WriteQueue::empty(); + let mut reporting = playing("the-film", &mut queue); + reporting.report( + ReportsWithoutWaiting::Paused, + played_to(4), + at(4), + &mut queue, + ); + let did = reporting.report( + ReportsWithoutWaiting::Resumed, + played_to(4), + at(3600), + &mut queue, + ); + + assert_eq!(did, WhatTheEnqueueDid::ReplacedInPlace); + assert_eq!( + reporting.observe(played_to(13), at(3609), &mut queue), + WhatObservingDid::NotDueYet + ); + assert_eq!( + reporting.observe(played_to(14), at(3610), &mut queue), + WhatObservingDid::Reported(WhatTheEnqueueDid::ReplacedInPlace) + ); + } + + /// A position observed before anything started is not a report about + /// something that is not happening. + #[test] + fn nothing_is_reported_before_playback_started() { + let mut queue = WriteQueue::empty(); + let mut reporting = Reporting::for_item(item("the-film"), at(0)); + + assert_eq!( + reporting.observe(played_to(0), at(30), &mut queue), + WhatObservingDid::NothingIsPlaying + ); + assert!(queue.is_empty()); + assert_eq!(reporting.target(), &item("the-film")); + } + + /// 0056's rule that the core always states the position, on every report. + /// A report at the beginning carries the beginning, which is a position and + /// not an absence, and the type has no absent value to carry instead. + #[test] + fn a_report_always_states_the_position() { + let mut queue = WriteQueue::empty(); + let mut reporting = Reporting::for_item(item("the-film"), at(0)); + reporting.report( + ReportsWithoutWaiting::Started, + played_to(0), + at(0), + &mut queue, + ); + + let entry = the_entry_for("the-film", &queue); + assert_eq!(entry.position(), Ticks::ZERO); + assert_eq!( + entry.reported_on(), + ReportedOn::Event(ReportsWithoutWaiting::Started) + ); + } +} diff --git a/tests/thread_statements.rs b/tests/thread_statements.rs index bb44dcf..3277f59 100644 --- a/tests/thread_statements.rs +++ b/tests/thread_statements.rs @@ -61,6 +61,7 @@ use flowfin_core::measurement::{Measurement, MeasurementSink}; use flowfin_core::playback::cadence::{ ReportsWithoutWaiting, TheInterval, WhatItDoesToTheInterval, }; +use flowfin_core::playback::report::{PositionReport, ReportedOn, Reporting, WhatObservingDid}; use flowfin_core::server::address::{AddressNotUsable, BaseAddress}; use flowfin_core::server::destinations::{ AdmittedOrigin, Destinations, WhatARedirectDoes, WhatConfiguringDid, @@ -632,6 +633,26 @@ fn the_progress_reporting_interval_is_safe_from_any_thread() { const _: () = any_thread::(); } +#[test] +fn what_occasioned_a_position_report_is_safe_from_any_thread() { + const _: () = any_thread::(); +} + +#[test] +fn a_position_report_is_safe_from_any_thread() { + const _: () = any_thread::(); +} + +#[test] +fn what_observing_a_position_did_is_safe_from_any_thread() { + const _: () = any_thread::(); +} + +#[test] +fn one_items_reporting_is_safe_from_any_thread() { + const _: () = any_thread::(); +} + #[test] fn what_announcing_a_window_did_is_safe_from_any_thread() { const _: () = any_thread::();