@@ -233,7 +233,7 @@ TEST(SinkRecovery, ACallerThatReportsNothingGetsExactlyOneRescan) {
233233 escalate (recovery);
234234 ASSERT_GE (rescan_fires_at (recovery, T0 , T0 + 10 * SINK_RESCAN_DELAY_MS ), T0 );
235235
236- // A caller that never reports gets exactly one rescan, as PortAudioSink relies on .
236+ // A caller that never reports gets exactly one rescan.
237237 EXPECT_FALSE (recovery.reopen_due ());
238238 EXPECT_FALSE (recovery.pending ());
239239 EXPECT_EQ (rescan_fires_at (recovery, T0 , T0 + 100 * SINK_RESCAN_DELAY_MS ), -1 );
@@ -251,6 +251,84 @@ TEST(SinkRecovery, ASuccessfulRescanIsNotRetried) {
251251 EXPECT_EQ (rescan_fires_at (recovery, fired_at, fired_at + 100 * SINK_RESCAN_DELAY_MS ), -1 );
252252}
253253
254+ TEST (SinkRecovery, ASecondOutageInTheSameStreamRecovers) {
255+ SinkRecovery recovery;
256+ escalate (recovery);
257+ int64_t now = rescan_fires_at (recovery, T0 , T0 + 10 * SINK_RESCAN_DELAY_MS );
258+ ASSERT_GT (now, 0 );
259+ recovery.rescan_done (false );
260+ now = rescan_fires_at (recovery, now, now + 10 * SINK_RESCAN_DELAY_MS );
261+ ASSERT_GT (now, 0 );
262+ recovery.rescan_done (true );
263+
264+ // The device died again: the reopen is back in hand, and so is a fresh ladder.
265+ escalate (recovery);
266+ EXPECT_TRUE (recovery.pending ());
267+ EXPECT_EQ (rescan_fires_at (recovery, now, now + 10 * SINK_RESCAN_DELAY_MS ),
268+ now + SINK_RESCAN_DELAY_MS );
269+ }
270+
271+ TEST (SinkRecovery, EveryOutageAfterARecoveryStillGivesUp) {
272+ SinkRecovery recovery;
273+ escalate (recovery);
274+ int64_t now = rescan_fires_at (recovery, T0 , T0 + 10 * SINK_RESCAN_DELAY_MS );
275+ ASSERT_GT (now, 0 );
276+ recovery.rescan_done (true );
277+
278+ escalate (recovery);
279+ for (int attempt = 0 ; attempt < SINK_RESCAN_ATTEMPTS ; ++attempt) {
280+ now = rescan_fires_at (recovery, now, now + 100 * SINK_RESCAN_DELAY_MS );
281+ ASSERT_GT (now, 0 ) << " attempt " << attempt << " never fired" ;
282+ recovery.rescan_done (false );
283+ }
284+
285+ EXPECT_FALSE (recovery.pending ());
286+ EXPECT_FALSE (recovery.reopen_due ());
287+ EXPECT_EQ (rescan_fires_at (recovery, now, now + 100 * SINK_RESCAN_DELAY_MS ), -1 );
288+ }
289+
290+ TEST (SinkRecovery, TheDiscardedGapSurvivesTheRefillIntoTheNextOutage) {
291+ SinkRecovery recovery;
292+ escalate (recovery);
293+ recovery.discard_frames (48'000U );
294+ ASSERT_GE (rescan_fires_at (recovery, T0 , T0 + 10 * SINK_RESCAN_DELAY_MS ), T0 );
295+ recovery.rescan_done (true );
296+
297+ // Died again before a timed write took the first gap: both are still owed.
298+ escalate (recovery);
299+ recovery.discard_frames (1'000U );
300+
301+ EXPECT_EQ (recovery.take_discarded_frames (), 49'000U );
302+ }
303+
304+ TEST (SinkRecovery, AnAbandonedRescanRefillsNothing) {
305+ SinkRecovery recovery;
306+ escalate (recovery);
307+ const int64_t fired_at = rescan_fires_at (recovery, T0 , T0 + 10 * SINK_RESCAN_DELAY_MS );
308+ ASSERT_GT (fired_at, 0 );
309+
310+ recovery.rescan_abandoned ();
311+
312+ EXPECT_FALSE (recovery.pending ());
313+ EXPECT_FALSE (recovery.reopen_due ());
314+ EXPECT_EQ (rescan_fires_at (recovery, fired_at, fired_at + 100 * SINK_RESCAN_DELAY_MS ), -1 );
315+
316+ // A late recovered report has no attempt to answer, so it cannot refill either.
317+ recovery.rescan_done (true );
318+ EXPECT_FALSE (recovery.reopen_due ());
319+ }
320+
321+ TEST (SinkRecovery, AbandoningWithNothingOutstandingDoesNothing) {
322+ SinkRecovery recovery;
323+ escalate (recovery);
324+
325+ recovery.rescan_abandoned ();
326+
327+ EXPECT_TRUE (recovery.pending ());
328+ EXPECT_EQ (rescan_fires_at (recovery, T0 , T0 + 10 * SINK_RESCAN_DELAY_MS ),
329+ T0 + SINK_RESCAN_DELAY_MS );
330+ }
331+
254332TEST (SinkRecovery, AFailedRescanIsTriedAgainAfterALongerDelay) {
255333 SinkRecovery recovery;
256334 escalate (recovery);
0 commit comments