import Testing import Foundation @testable import OnyxHelperProtocol // Das Sicherheitsnetz ist der einzige Teil dieses Projekts, dessen Versagen // Hardware kosten kann. Es liegt deshalb als reine Funktion vor, wird hier // vollständig durchgeprüft und sitzt im Helfer — nicht in der Oberfläche, die // abstürzen, eingefroren oder beendet sein kann, während die Lüfter stehen. @Suite("Lüfter-Sicherheitsnetz") struct FanSafetyTests { private let limits = FanLimits(minimum: 2317, maximum: 7826) private let now = Date(timeIntervalSince1970: 1_800_000_000) private func decide(rpm: Double?, secondsSinceHeartbeat: TimeInterval = 0, hottest: Double? = 50) -> FanDecision { FanSafety.decide(request: rpm.map { FanRequest(targetRPM: $0) }, lastHeartbeat: now.addingTimeInterval(-secondsSinceHeartbeat), now: now, hottestCelsius: hottest, limits: limits) } // MARK: - Der Normalfall @Test("Ohne Anforderung läuft die Automatik") func noRequestMeansAuto() { #expect(decide(rpm: nil) == .automatic(reason: .noRequest)) } @Test("Eine Anforderung im gültigen Bereich wird durchgereicht") func validRequestIsHonoured() { #expect(decide(rpm: 4000) == .manual(rpm: 4000)) } // MARK: - Grenzen @Test("Unter dem Firmware-Minimum wird auf das Minimum angehoben") func belowMinimumIsRaised() { // Der wichtigste Einzelfall: ein Lüfter, der langsamer läuft als vom // Hersteller vorgesehen, kann den Rechner überhitzen, ohne dass die // Automatik eingreift — sie ist ja abgeschaltet. #expect(decide(rpm: 500) == .manual(rpm: 2317)) } @Test("Null wird nicht als 'aus' verstanden, sondern angehoben") func zeroIsNotOff() { #expect(decide(rpm: 0) == .manual(rpm: 2317)) } @Test("Ein negativer Wert wird ebenfalls angehoben") func negativeIsRaised() { #expect(decide(rpm: -1000) == .manual(rpm: 2317)) } @Test("Über dem Maximum wird gedeckelt") func aboveMaximumIsCapped() { #expect(decide(rpm: 99999) == .manual(rpm: 7826)) } @Test("Genau auf den Grenzen bleibt der Wert unverändert") func exactBoundsPassThrough() { #expect(decide(rpm: 2317) == .manual(rpm: 2317)) #expect(decide(rpm: 7826) == .manual(rpm: 7826)) } // MARK: - Wachhund @Test("Schweigt die App zu lange, fällt der Lüfter auf Automatik zurück") func watchdogReturnsToAuto() { // Der Fall, für den das Netz vor allem da ist: Onyx wird hart beendet // oder stürzt ab, während die Lüfter auf manuell stehen. Ohne Wachhund // bliebe die Einstellung bis zum Neustart bestehen. #expect(decide(rpm: 4000, secondsSinceHeartbeat: 6) == .automatic(reason: .watchdog)) } @Test("Kurz vor Ablauf gilt die Anforderung noch") func justBeforeTimeoutStillManual() { #expect(decide(rpm: 4000, secondsSinceHeartbeat: 4.9) == .manual(rpm: 4000)) } @Test("Genau auf der Frist gilt sie noch") func exactlyAtTimeoutStillManual() { #expect(decide(rpm: 4000, secondsSinceHeartbeat: FanSafety.watchdogTimeout) == .manual(rpm: 4000)) } // MARK: - Temperaturwächter @Test("Über dem Grenzwert übernimmt die Automatik, egal was angefordert war") func temperatureOverridesRequest() { #expect(decide(rpm: 2400, hottest: 96) == .automatic(reason: .temperature)) } @Test("Der Temperaturwächter schlägt auch eine Anforderung auf Höchstdrehzahl") func temperatureOverridesEvenMaximum() { // Auch bei voller Drehzahl übernimmt die Automatik: sie kennt // Drosselung und Lastverteilung, ein festgenagelter Wert nicht. #expect(decide(rpm: 7826, hottest: 100) == .automatic(reason: .temperature)) } @Test("Knapp unter dem Grenzwert bleibt es bei der Anforderung") func justBelowThresholdStaysManual() { #expect(decide(rpm: 3000, hottest: 94.9) == .manual(rpm: 3000)) } @Test("Ohne Temperaturmesswert wird nicht gesteuert") func missingTemperatureIsUnsafe() { // Ohne Temperatur lässt sich nicht beurteilen, ob eine feste Drehzahl // gerade gefährlich ist. Blind weiterzudrehen wäre die falsche Antwort. #expect(decide(rpm: 3000, hottest: nil) == .automatic(reason: .noTemperature)) } // MARK: - Rangfolge @Test("Temperatur schlägt Wachhund — beide führen zur Automatik, der Grund zählt") func temperatureWinsOverWatchdog() { let decision = FanSafety.decide(request: FanRequest(targetRPM: 3000), lastHeartbeat: now.addingTimeInterval(-60), now: now, hottestCelsius: 99, limits: limits) #expect(decision == .automatic(reason: .temperature)) } @Test("Unsinnige Grenzen führen zur Automatik statt zu einem geratenen Wert") func brokenLimitsFallBack() { // Kommt vor, wenn die Firmware-Keys nicht lesbar sind. Dann ist der // erlaubte Bereich unbekannt, und eine Drehzahl zu setzen wäre geraten. let decision = FanSafety.decide(request: FanRequest(targetRPM: 3000), lastHeartbeat: now, now: now, hottestCelsius: 50, limits: FanLimits(minimum: 0, maximum: 0)) #expect(decision == .automatic(reason: .unknownLimits)) } @Test("Die Grenzwerte entsprechen dem Plan") func constantsMatchPlan() { #expect(FanSafety.watchdogTimeout == 5) #expect(FanSafety.criticalCelsius == 95) } }