Files
Flaro/Packages/FlixyKit/Tests/FlixyPlaybackTests/UpgradeRuleTests.swift
T
ScarriffleandClaude Opus 5 4323c47db6 Schneller anfangen, dann hinaufgehen
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>
2026-09-29 06:25:15 +02:00

144 lines
5.4 KiB
Swift

import Foundation
import Testing
@testable import FlixyPlayback
@Suite("Wann hinaufgeschaltet wird")
struct UpgradeRuleTests {
/// Die Lage, in der es erlaubt ist — alle übrigen Tests weichen davon in
/// genau einem Punkt ab. So steht in jedem Test nur der Unterschied.
private func ruhig(
isAutomatic: Bool = true,
condition: StreamCondition = .healthy,
hasHigherRung: Bool = false,
ceilingAllowsMore: Bool = true,
bufferedAhead: Duration? = .seconds(30),
playedFor: Duration = .seconds(120),
sinceLastStall: Duration? = nil,
sinceLastSeek: Duration? = nil,
sinceLastUpgrade: Duration? = nil,
used: Int = 0,
budget: Int = 2
) -> UpgradeRule.Facts {
.init(
isAutomatic: isAutomatic,
condition: condition,
hasHigherRung: hasHigherRung,
ceilingAllowsMore: ceilingAllowsMore,
bufferedAhead: bufferedAhead,
playedFor: playedFor,
sinceLastStall: sinceLastStall,
sinceLastSeek: sinceLastSeek,
sinceLastUpgrade: sinceLastUpgrade,
used: used,
budget: budget
)
}
@Test("Läuft es ruhig und ist die Leiter ausgereizt, wird neu aufgebaut")
func calmAndCappedRebuilds() {
#expect(UpgradeRule.verdict(ruhig()) == .rebuildHigher)
}
// MARK: - Was es verbietet
/// Wer „720p · 3 Mbps" antippt, meint das — auch wenn die Leitung mehr
/// trüge.
@Test("Eine gewählte Stufe wird nicht überstimmt")
func neverOverridesAChoice() {
#expect(UpgradeRule.verdict(ruhig(isAutomatic: false)) == .notAutomatic)
}
/// **Der wichtigste Fall.** Solange die Leiter noch eine Sprosse hat,
/// kostet das Hinaufgehen kein schwarzes Bild — ein Neuaufbau dafür wäre
/// Verschwendung.
@Test("Solange die Leiter trägt, kein Neuaufbau")
func prefersTheCheapPath() {
#expect(UpgradeRule.verdict(ruhig(hasHigherRung: true)) == .cheaperPathRemains)
}
@Test("Über die eingestellte Grenze geht es nicht")
func respectsTheSetting() {
#expect(UpgradeRule.verdict(ruhig(ceilingAllowsMore: false)) == .atCeiling)
}
@Test("Bei angespannter Lage wird nicht hinaufgegangen")
func notWhenStrained() {
#expect(UpgradeRule.verdict(ruhig(condition: .strained)) == .notCalmEnough)
#expect(UpgradeRule.verdict(ruhig(condition: .failing)) == .notCalmEnough)
}
/// „Es läuft gerade" genügt nicht — es muss etwas auf der hohen Kante
/// liegen, sonst trägt es den Neuaufbau nicht.
@Test("Ohne Vorrat kein Neuaufbau")
func needsABuffer() {
#expect(UpgradeRule.verdict(ruhig(bufferedAhead: .seconds(5))) == .notCalmEnough)
#expect(UpgradeRule.verdict(ruhig(bufferedAhead: nil)) == .notCalmEnough)
}
/// Der Puffer füllt sich nach dem Start immer, und das sieht eine halbe
/// Minute lang nach einer hervorragenden Leitung aus.
@Test("Am Anfang nicht, auch wenn alles gut aussieht")
func notRightAfterStart() {
#expect(UpgradeRule.verdict(ruhig(playedFor: .seconds(20))) == .tooEarly)
}
@Test("Nach einem Sprung nicht")
func notRightAfterSeek() {
#expect(UpgradeRule.verdict(ruhig(sinceLastSeek: .seconds(3))) == .tooEarly)
}
/// Wer gerade gestockt hat, hat keine Reserve — und ein Neuaufbau wäre
/// die zweite Unterbrechung binnen einer Minute.
@Test("Nach einem Rückler lange nicht")
func waitsAfterAStall() {
#expect(UpgradeRule.verdict(ruhig(sinceLastStall: .seconds(30))) == .tooEarly)
#expect(UpgradeRule.verdict(ruhig(sinceLastStall: .seconds(120))) == .rebuildHigher)
}
@Test("Die Sperrfrist gilt")
func respectsTheCooldown() {
#expect(UpgradeRule.verdict(ruhig(sinceLastUpgrade: .seconds(60))) == .cooling)
#expect(UpgradeRule.verdict(ruhig(sinceLastUpgrade: .seconds(200))) == .rebuildHigher)
}
@Test("Das Budget ist endlich")
func budgetIsFinite() {
#expect(UpgradeRule.verdict(ruhig(used: 2, budget: 2)) == .budgetSpent)
#expect(UpgradeRule.verdict(ruhig(used: 1, budget: 2)) == .rebuildHigher)
}
// MARK: - Die Reihenfolge der Gründe
/// Bei mehreren Gründen soll der **grundsätzlichste** gemeldet werden —
/// sonst steht im Protokoll „Sperrfrist", wo in Wahrheit die Einstellung
/// das Ende ist, und jemand wartet auf etwas, das nie kommt.
@Test("Der grundsätzlichste Grund gewinnt")
func reportsTheMostFundamentalReason() {
#expect(
UpgradeRule.verdict(ruhig(
isAutomatic: false, hasHigherRung: true, ceilingAllowsMore: false
)) == .notAutomatic
)
#expect(
UpgradeRule.verdict(ruhig(
hasHigherRung: true, ceilingAllowsMore: false
)) == .atCeiling
)
#expect(
UpgradeRule.verdict(ruhig(
condition: .failing, hasHigherRung: true
)) == .cheaperPathRemains
)
}
/// Das Hinaufschalten muss zurückhaltender sein als das Absenken: ein zu
/// spätes Absenken bedeutet Ruckeln, ein zu spätes Hinaufschalten ein
/// etwas weicheres Bild.
@Test("Zurückhaltender als das Absenken")
func moreCautiousThanDowngrading() {
#expect(UpgradeRule.settleIn > DowngradeRule.settleIn)
#expect(UpgradeRule.cooldown < DowngradeRule.cooldown)
}
}