import Foundation import Testing @testable import FlaroPlayback @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) } }