Reine Umbenennung, kein Verhalten geändert — bewusst als eigener Commit, damit „umbenannt" und „umgebaut" im Nachhinein auseinanderzuhalten sind. 712 mal `Flixy`, 778 mal `flixy`, 7 mal `FLIXY` in 169 Dateien, dazu 25 Pfade. Ziele: `Halo`, `HaloTV`, `HaloTopShelf`, `HaloUITests`, `HaloKit` mit `HaloDesign`, `HaloDownloads`, `HaloPlayback` und `HaloProbe`. `MediaKit`, `JellyfinKit` und `DownloadWidget` tragen den Namen nicht und bleiben. `docs/superpowers/specs/` ist ausgenommen. Datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen; sie nachträglich umzuschreiben hiesse, die Vergangenheit zu glätten. **Die Bundle-ID wandert mit** — ausdrücklich so gewollt, mit bekanntem Preis: `com.scarriffleservices.halo` ist für iOS eine *andere* App. Die installierte Fassung bleibt daneben stehen, mit Downloads und Anmeldung in ihrem Container; Halo startet leer. Einmal neu anmelden, heruntergeladene Folgen einmal neu laden. Dasselbe gilt für die App-Gruppe, die sich TV-App und Top Shelf teilen. **Eine Folge davon, die beim Entscheiden noch nicht auf dem Tisch lag:** der Aufräumschritt aus dem vorigen Commit kommt an die alten Plex-Token nicht mehr heran. Der Schlüsselbund grenzt nach Team und Bundle-Kennung ab — was unter `…flixy` liegt, ist für `…halo` unerreichbar. Erledigt wird es trotzdem, nur anders: **die alte App löschen.** iOS räumt deren Schlüsselbund-Einträge beim Deinstallieren mit ab. Der Code bleibt stehen, weil er für jeden Stand vor dieser Umbenennung richtig ist und im Jahr einen Startvergleich kostet. Die Projektdatei ist aus `project.yml` neu erzeugt, der alte Ordner gelöscht statt umbenannt — sie steht ohnehin nicht im Repo. Das Verzeichnis des Repos selbst heißt weiter `Flixy`; das umzubenennen ist ein Handgriff ausserhalb des Projekts. 368 Tests grün. iOS und tvOS bauen. 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 HaloPlayback
|
|
|
|
@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)
|
|
}
|
|
}
|