Files
Flaro/Packages/HaloKit/Tests/HaloDownloadsTests/DownloadRateTests.swift
T
ScarriffleandClaude Opus 5 4b9a1e7b67 Flixy heißt ab hier Halo
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>
2026-10-03 11:12:32 +02:00

119 lines
4.8 KiB
Swift

import Testing
@testable import HaloDownloads
@Suite("Downloadrate und Restzeit")
struct DownloadRateTests {
/// Ein fester Anfangszeitpunkt, von dem aus gerechnet wird.
///
/// Die Uhr kommt von außen herein — deshalb braucht hier nichts zu warten,
/// und eine Übertragung über eine Viertelstunde ist in Mikrosekunden
/// nachgestellt.
private let start = ContinuousClock.now
@Test("Vor dem ersten Abstand gibt es keine Zahl")
func noRateFromASinglePoint() {
var rate = DownloadRate()
rate.record(bytes: 0, at: start)
// Null wäre eine Auskunft („es geht nichts durch"), und die stimmt
// nicht — es ist nur noch nichts gemessen.
#expect(rate.bytesPerSecond == nil)
}
@Test("Gleichmäßiger Zufluss ergibt eine ruhige Zahl")
func steadyFlowIsSteady() {
var rate = DownloadRate()
for sekunde in 0...5 {
rate.record(bytes: Int64(sekunde) * 1_000_000, at: start.advanced(by: .seconds(sekunde)))
}
// Auf das Byte genau: die Glättung zieht einen Messwert nur dann in
// eine Richtung, wenn er vom bisherigen abweicht.
#expect(rate.bytesPerSecond == 1_000_000)
}
@Test("Ein Ausreißer verschiebt die Zahl, aber reißt sie nicht mit")
func outlierIsDamped() {
var rate = DownloadRate()
for sekunde in 0...5 {
rate.record(bytes: Int64(sekunde) * 1_000_000, at: start.advanced(by: .seconds(sekunde)))
}
// Eine einzelne Sekunde mit dem Zehnfachen.
rate.record(bytes: 15_000_000, at: start.advanced(by: .seconds(6)))
let gemessen = try! #require(rate.bytesPerSecond)
// Roh wären es zehn Millionen. Geglättet liegt die Zahl dazwischen —
// sie folgt, statt zu springen.
#expect(gemessen > 1_000_000)
#expect(gemessen < 10_000_000)
}
@Test("Messpunkte dichter als eine halbe Sekunde zählen nicht")
func samplesTooCloseAreIgnored() {
var rate = DownloadRate()
rate.record(bytes: 0, at: start)
rate.record(bytes: 50_000, at: start.advanced(by: .milliseconds(100)))
rate.record(bytes: 100_000, at: start.advanced(by: .milliseconds(200)))
#expect(rate.bytesPerSecond == nil)
// Und der Bezugspunkt ist dabei nicht weitergerückt: gemessen wird
// über die volle Sekunde seit dem ersten Punkt, nicht über die letzten
// achthundert Millisekunden.
rate.record(bytes: 1_000_000, at: start.advanced(by: .seconds(1)))
#expect(rate.bytesPerSecond == 1_000_000)
}
@Test("Eine lange Stille verfälscht die Rate nicht")
func longGapResetsTheMeasurement() {
var rate = DownloadRate()
rate.record(bytes: 0, at: start)
rate.record(bytes: 1_000_000, at: start.advanced(by: .seconds(1)))
#expect(rate.bytesPerSecond == 1_000_000)
// Eine halbe Minute ohne Rückmeldung heißt nicht „langsam", sondern
// „stand still": App angehalten, Gerät gesperrt, Netz weg. Daraus eine
// Rate zu rechnen ergäbe dreißig Kilobyte je Sekunde und eine Restzeit
// von Stunden.
rate.record(bytes: 1_030_000, at: start.advanced(by: .seconds(31)))
#expect(rate.bytesPerSecond == nil)
}
@Test("Fängt die Übertragung von vorn an, gilt keine alte Zahl")
func restartFromZeroDiscardsTheRate() {
var rate = DownloadRate()
rate.record(bytes: 0, at: start)
rate.record(bytes: 1_000_000, at: start.advanced(by: .seconds(1)))
#expect(rate.bytesPerSecond == 1_000_000)
// Genau das passiert, wenn ein gescheiterter Download ohne
// Wiederaufnahme-Datensatz neu beginnt.
rate.record(bytes: 0, at: start.advanced(by: .seconds(2)))
#expect(rate.bytesPerSecond == nil)
}
@Test("Die Restzeit folgt aus Rate und Rest")
func remainingFollowsFromRate() {
var rate = DownloadRate()
rate.record(bytes: 0, at: start)
rate.record(bytes: 1_000_000, at: start.advanced(by: .seconds(1)))
let rest = try! #require(rate.remaining(received: 1_000_000, total: 11_000_000))
#expect(rest == .seconds(10))
}
@Test("Ohne Rate keine Restzeit")
func noRemainingWithoutRate() {
var rate = DownloadRate()
rate.record(bytes: 0, at: start)
// Eine Restzeit ohne Messung wäre geraten — und geraten sieht man ihr
// nicht an.
#expect(rate.remaining(received: 0, total: 1_000_000) == nil)
}
@Test("Ist alles da, gibt es nichts mehr zu schätzen")
func noRemainingWhenComplete() {
var rate = DownloadRate()
rate.record(bytes: 0, at: start)
rate.record(bytes: 1_000_000, at: start.advanced(by: .seconds(1)))
#expect(rate.remaining(received: 1_000_000, total: 1_000_000) == nil)
}
}