import Testing import Foundation @testable import FlaroPlayback @Suite("Zustand des Streams") struct StreamHealthTests { /// Baut eine Folge von Proben im Sekundentakt. private func lauf( _ health: inout StreamHealth, von: Int, bis: Int, puffer: (Int) -> Double?, stalls: (Int) -> Int = { _ in 0 }, keepUp: (Int) -> Bool? = { _ in true } ) -> StreamCondition { var urteil = StreamCondition.healthy for sekunde in von...bis { urteil = health.record(StreamSample( at: .seconds(sekunde), bufferedAhead: puffer(sekunde).map { .milliseconds(Int($0 * 1000)) }, isLikelyToKeepUp: keepUp(sekunde), stallCount: stalls(sekunde) )) } return urteil } @Test("Ein voller, stabiler Puffer ist gesund") func fullBufferIsHealthy() { var health = StreamHealth() let urteil = lauf(&health, von: 0, bis: 60, puffer: { _ in 30 }) #expect(urteil == .healthy) } /// Der Fall, für den das Ganze gebaut ist: der Puffer schrumpft stetig. /// Das muss auffallen, **bevor** er leer ist. @Test("Ein schrumpfender Puffer meldet sich vor dem Rückler") func shrinkingBufferWarnsEarly() { var health = StreamHealth() // Start mit 30 s, dann jede Sekunde eine weniger. _ = lauf(&health, von: 0, bis: 10, puffer: { _ in 30 }) let urteil = lauf(&health, von: 11, bis: 36, puffer: { 30 - Double($0 - 10) }) #expect(urteil == .strained) // Und zwar bei noch fünf Sekunden Puffer — nicht bei null. #expect(health.condition != .failing) } @Test("Ein kleiner, aber stabiler Puffer ist kein Alarm") func smallStableBufferIsNotAlarming() { var health = StreamHealth() let urteil = lauf(&health, von: 0, bis: 30, puffer: { _ in 4 }) // Klein, aber er hält. Das ist kein Grund, die Stufe zu senken. #expect(urteil != .strained) #expect(urteil != .failing) } @Test("Zwei Rückler in einer Minute heißen: trägt nicht") func twoStallsMeanFailing() { var health = StreamHealth() let urteil = lauf( &health, von: 0, bis: 40, puffer: { _ in 10 }, stalls: { $0 < 10 ? 0 : ($0 < 25 ? 1 : 2) } ) #expect(urteil == .failing) } @Test("Ein leerer Puffer heißt sofort: trägt nicht") func emptyBufferIsFailing() { var health = StreamHealth() let urteil = lauf(&health, von: 0, bis: 20, puffer: { $0 < 18 ? 10 : 0 }) #expect(urteil == .failing) } /// „Noch nichts geladen" ist etwas anderes als „Puffer leer". Beim Start /// darf das nicht als Notlage durchgehen. @Test("Ohne Messwert kein Alarm") func unknownBufferIsNotAlarming() { var health = StreamHealth() let urteil = lauf(&health, von: 0, bis: 10, puffer: { _ in nil }) #expect(urteil == .healthy) } /// Der Weg zurück ist absichtlich schmal: schnell runter, langsam rauf. @Test("Zurück nach gesund braucht eine halbe Minute Ruhe") func recoveryIsSlow() { var health = StreamHealth() _ = lauf(&health, von: 0, bis: 20, puffer: { $0 < 18 ? 10 : 0 }) #expect(health.condition == .failing) // Puffer sofort wieder voll — aber zu kurz. let früh = lauf(&health, von: 21, bis: 40, puffer: { _ in 30 }) #expect(früh != .healthy) // Nach einer halben Minute durchgehend gut: erholt. let spät = lauf(&health, von: 41, bis: 90, puffer: { _ in 30 }) #expect(spät == .healthy) } /// Ein einzelner Ausschlag von `isLikelyToKeepUp` kommt bei jedem Sprung /// vor und darf nichts auslösen. @Test("Ein einzelner Ausschlag löst nichts aus") func singleKeepUpDipIsIgnored() { var health = StreamHealth() let urteil = lauf( &health, von: 0, bis: 30, puffer: { _ in 25 }, keepUp: { $0 == 15 ? false : true } ) #expect(urteil == .healthy) } @Test("Anhaltende Sorge des Systems zählt") func sustainedKeepUpFalseCounts() { var health = StreamHealth() let urteil = lauf( &health, von: 0, bis: 30, puffer: { _ in 25 }, keepUp: { $0 >= 27 ? false : true } ) #expect(urteil == .strained) } @Test("Zurücksetzen macht einen frischen Anfang") func resetStartsOver() { var health = StreamHealth() _ = lauf(&health, von: 0, bis: 20, puffer: { $0 < 18 ? 10 : 0 }) #expect(health.condition == .failing) health.removeAll() #expect(health.condition == .healthy) } }