import Testing import Foundation @testable import MetricsProvider @Suite("Akkuverlauf") struct BatteryHistoryTests { private func sample(_ seconds: TimeInterval, charge: Double) -> BatterySample { BatterySample(at: Date(timeIntervalSince1970: seconds), charge: charge, celsius: nil, watts: nil) } private let now = Date(timeIntervalSince1970: 100 * 86_400) @Test("Ohne vorherige Messung wird sofort aufgezeichnet") func firstSampleIsRecorded() { #expect(BatteryHistoryRules.shouldRecord(last: nil, now: now)) } @Test("Vor Ablauf des Abstands nicht noch einmal") func waitsForTheInterval() { let recent = now.addingTimeInterval(-60) #expect(!BatteryHistoryRules.shouldRecord(last: recent, now: now)) } @Test("Nach dem Abstand wieder") func recordsAfterTheInterval() { let old = now.addingTimeInterval(-BatteryHistoryRules.interval) #expect(BatteryHistoryRules.shouldRecord(last: old, now: now)) } @Test("Zu alte Messpunkte fallen heraus") func oldSamplesArePruned() { let samples = [ sample(now.timeIntervalSince1970 - 40 * 86_400, charge: 0.5), sample(now.timeIntervalSince1970 - 1 * 86_400, charge: 0.6), ] let kept = BatteryHistoryRules.pruned(samples, now: now) #expect(kept.count == 1) #expect(kept.first?.charge == 0.6) } @Test("Die Säulen bekommen den Mittelwert ihres Abschnitts") func bucketsAverage() { // Zwei Messungen in derselben halben Stunde ergeben eine Säule mit // ihrem Mittel — nicht die erste und nicht die letzte. let base = now.timeIntervalSince1970 - 600 let samples = [sample(base, charge: 0.4), sample(base + 60, charge: 0.6)] let buckets = BatteryHistoryRules.buckets(samples, window: .day, now: now) let filled = buckets.compactMap { $0 } #expect(filled.count == 1) #expect(abs(filled[0] - 0.5) < 0.0001) } @Test("Abschnitte ohne Messung bleiben leer") func gapsStayEmpty() { // Ein zugeklappter Rechner hat dort keinen Ladestand gehabt. Eine // durchgezogene Linie darüber wäre erfunden. let samples = [sample(now.timeIntervalSince1970 - 600, charge: 0.5)] let buckets = BatteryHistoryRules.buckets(samples, window: .day, now: now) #expect(buckets.count == HistoryWindow.day.buckets) #expect(buckets.filter { $0 == nil }.count == HistoryWindow.day.buckets - 1) } @Test("Messpunkte außerhalb des Fensters zählen nicht mit") func outOfWindowIsIgnored() { let samples = [sample(now.timeIntervalSince1970 - 10 * 86_400, charge: 0.9)] #expect(BatteryHistoryRules.buckets(samples, window: .day, now: now).allSatisfy { $0 == nil }) #expect(BatteryHistoryRules.summary(samples, window: .day, now: now) == nil) } @Test("Die Zusammenfassung nennt Tief, Hoch und Mittel") func summaryReportsRange() { let base = now.timeIntervalSince1970 - 3600 let samples = [sample(base, charge: 0.2), sample(base + 60, charge: 0.8)] let summary = BatteryHistoryRules.summary(samples, window: .day, now: now) #expect(summary?.low == 0.2) #expect(summary?.high == 0.8) #expect(abs((summary?.average ?? 0) - 0.5) < 0.0001) } @Test("Jedes Fenster hat weniger Säulen als Messpunkte") func bucketsAreFewerThanSamples() { // Über einen Monat wären es bei fünf Minuten Abstand über achttausend // Punkte — mehr Punkte als Bildpunkte zu zeichnen kostet Zeit, ohne // etwas zu zeigen. for window in HistoryWindow.allCases { let possible = Int(window.duration / BatteryHistoryRules.interval) #expect(window.buckets < possible) } } }