Es dauerte eine halbe Minute bis zum ersten Bild. Der Grund war nicht die Leitung, sondern der Server: „Auto\" begann mit acht Megabit, und das heisst 1080p von der ersten Sekunde an. So lange braucht er, um das zu rechnen. Jetzt wird mit drei Megabit begonnen — also 720p. Das laeuft in wenigen Sekunden an, sieht einen Augenblick lang weicher aus, und von dort geht es hinauf, sobald sich zeigt, dass die Leitung mehr traegt. Dabei steckt eine Falle, die den ganzen Gewinn aufgezehrt haette: der Server baut seine Leiter aus der Bestellung. Wer mit drei Megabit anfaengt, bekommt eine Leiter, deren oberste Sprosse drei Megabit ist — und der billige Hebel bewegt sich nur **innerhalb** dieser Leiter. Aus dem schnellen Start waere ein Film geworden, der den ganzen Abend in 720p bleibt. Hoeher geht es deshalb ueber einen Neuaufbau, und der braucht Regeln: nicht zu frueh, nicht nach einem Ruckler, nicht ohne Vorrat, nicht oefter als zweimal. Sie stehen in `UpgradeRule` — reiner Typ, dreizehn Tests, wie beim Absenken. Und deutlich zurueckhaltender als dort: ein zu spaetes Absenken bedeutet Ruckeln, ein zu spaetes Hinaufschalten ein etwas weicheres Bild. Das erste ist ein Fehler, das zweite eine Geschmacksfrage. Bei Live-TV wird nicht hinaufgegangen: dort kostet jeder Neuaufbau den Tuner, und ein schaerferes Bild ist diesen Einsatz nicht wert. `StallLedger` kann jetzt sagen, wann der letzte Ruckler war — das brauchte die Regel, und „noch nie gestockt\" ist etwas anderes als „vor langer Zeit\". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
4.5 KiB
Swift
116 lines
4.5 KiB
Swift
import Testing
|
|
import Foundation
|
|
@testable import FlixyPlayback
|
|
|
|
@Suite("Rückler-Buchführung")
|
|
struct StallLedgerTests {
|
|
/// Der Fall, wegen dem es den Typ gibt.
|
|
///
|
|
/// Zehn Rückler à dreißig Sekunden lösten beim alten Watchdog nichts aus,
|
|
/// weil er sich bei jedem Erholen zurücksetzte. Hier muss sichtbar
|
|
/// werden, dass die Wiedergabe unbrauchbar ist.
|
|
@Test("Viele kurze Rückler bleiben sichtbar")
|
|
func manyShortStallsAreVisible() {
|
|
var ledger = StallLedger(window: .seconds(180))
|
|
|
|
// Alle 20 s ein Rückler von 5 s, drei Minuten lang.
|
|
for step in 1...9 {
|
|
ledger.record(.init(endedAt: .seconds(step * 20), length: .seconds(5)))
|
|
}
|
|
|
|
let now = Duration.seconds(180)
|
|
#expect(ledger.count(at: now) == 9)
|
|
// 45 s Warten in 180 s Fenster.
|
|
#expect(ledger.share(at: now) > 0.2)
|
|
#expect(ledger.share(at: now) < 0.3)
|
|
}
|
|
|
|
@Test("Was aus dem Fenster fällt, zählt nicht mehr")
|
|
func oldEpisodesFallOut() {
|
|
var ledger = StallLedger(window: .seconds(180))
|
|
ledger.record(.init(endedAt: .seconds(10), length: .seconds(30)))
|
|
|
|
#expect(ledger.count(at: .seconds(60)) == 1)
|
|
// Weit hinter dem Fenster: vergessen.
|
|
#expect(ledger.count(at: .seconds(300)) == 0)
|
|
#expect(ledger.share(at: .seconds(300)) == 0)
|
|
}
|
|
|
|
/// Am Anfang ist das Fenster noch nicht voll — dann gegen die tatsächlich
|
|
/// vergangene Zeit rechnen, sonst sieht ein früher Rückler harmlos aus.
|
|
@Test("Früh im Film zählt die vergangene Zeit, nicht das ganze Fenster")
|
|
func earlyStallsAreNotDiluted() {
|
|
var ledger = StallLedger(window: .seconds(180))
|
|
ledger.record(.init(endedAt: .seconds(20), length: .seconds(5)))
|
|
|
|
let share = ledger.share(at: .seconds(20))
|
|
// 5 s von 20 s, nicht 5 s von 180 s.
|
|
#expect(share > 0.2)
|
|
#expect(share < 0.3)
|
|
}
|
|
|
|
@Test("Der laufende Rückler zählt mit")
|
|
func ongoingStallCounts() {
|
|
var ledger = StallLedger(window: .seconds(180))
|
|
ledger.record(.init(endedAt: .seconds(60), length: .seconds(10)))
|
|
|
|
let without = ledger.share(at: .seconds(180))
|
|
let with = ledger.share(at: .seconds(180), ongoing: .seconds(20))
|
|
#expect(with > without)
|
|
}
|
|
|
|
/// Ein einzelner langer Rückler ist eine andere Aussage als viele kurze.
|
|
@Test("Der längste Einzelfall lässt sich getrennt ablesen")
|
|
func longestIsSeparate() {
|
|
var ledger = StallLedger(window: .seconds(180))
|
|
ledger.record(.init(endedAt: .seconds(30), length: .seconds(2)))
|
|
ledger.record(.init(endedAt: .seconds(60), length: .seconds(18)))
|
|
ledger.record(.init(endedAt: .seconds(90), length: .seconds(3)))
|
|
|
|
#expect(ledger.longest(at: .seconds(120)) == .seconds(18))
|
|
#expect(ledger.count(at: .seconds(120)) == 3)
|
|
}
|
|
|
|
@Test("Eine ruhige Wiedergabe meldet nichts")
|
|
func quietPlaybackIsQuiet() {
|
|
let ledger = StallLedger()
|
|
#expect(ledger.count(at: .seconds(600)) == 0)
|
|
#expect(ledger.share(at: .seconds(600)) == 0)
|
|
#expect(ledger.longest(at: .seconds(600)) == .zero)
|
|
}
|
|
|
|
@Test("Der Anteil übersteigt nie eins")
|
|
func shareIsBounded() {
|
|
var ledger = StallLedger(window: .seconds(60))
|
|
// Absurd: mehr Wartezeit als Fenster.
|
|
ledger.record(.init(endedAt: .seconds(30), length: .seconds(200)))
|
|
#expect(ledger.share(at: .seconds(60)) == 1)
|
|
}
|
|
|
|
/// Gebraucht fürs Hinaufschalten: wer gerade gestockt hat, hat keine
|
|
/// Reserve für einen Neuaufbau.
|
|
@Test("Der letzte Rückler lässt sich datieren")
|
|
func findsTheLastStall() {
|
|
var buch = StallLedger(window: .seconds(180))
|
|
buch.record(.init(endedAt: .seconds(30), length: .seconds(2)))
|
|
buch.record(.init(endedAt: .seconds(90), length: .seconds(1)))
|
|
|
|
#expect(buch.lastStall(at: .seconds(100)) == .seconds(90))
|
|
}
|
|
|
|
/// „Noch nie gestockt" ist etwas anderes als „vor langer Zeit" — wer
|
|
/// keinen hatte, wartet auf keine Frist.
|
|
@Test("Ohne Rückler gibt es kein Datum")
|
|
func noStallNoDate() {
|
|
let leer = StallLedger(window: .seconds(180))
|
|
#expect(leer.lastStall(at: .seconds(100)) == nil)
|
|
}
|
|
|
|
@Test("Was aus dem Fenster gefallen ist, zählt nicht mehr")
|
|
func forgetsOldStalls() {
|
|
var buch = StallLedger(window: .seconds(60))
|
|
buch.record(.init(endedAt: .seconds(10), length: .seconds(2)))
|
|
#expect(buch.lastStall(at: .seconds(200)) == nil)
|
|
}
|
|
}
|