import Testing import Foundation @testable import MetricsProvider // Alle Messwerte hier sind Differenzen zwischen zwei Abtastungen. Das klingt // harmlos und ist die Stelle, an der Systemmonitore falsche Zahlen anzeigen: // die Zähler sind 32 Bit breit und laufen über, Intervalle sind nie exakt // gleich lang, und beim Interface-Wechsel fangen sie wieder bei null an. @Suite("CPU-Auslastung") struct CPUUsageTests { @Test("Halb beschäftigt, halb untätig ergibt 50 Prozent") func halfBusy() { let before = CPUTicks(user: 100, system: 0, idle: 100, nice: 0) let after = CPUTicks(user: 200, system: 0, idle: 200, nice: 0) #expect(CPUTicks.usage(from: before, to: after) == 0.5) } @Test("Nur Leerlauf ergibt null") func onlyIdle() { let before = CPUTicks(user: 0, system: 0, idle: 0, nice: 0) let after = CPUTicks(user: 0, system: 0, idle: 100, nice: 0) #expect(CPUTicks.usage(from: before, to: after) == 0) } @Test("Kein Leerlauf ergibt volle Auslastung") func fullyBusy() { let before = CPUTicks(user: 0, system: 0, idle: 50, nice: 0) let after = CPUTicks(user: 100, system: 0, idle: 50, nice: 0) #expect(CPUTicks.usage(from: before, to: after) == 1) } @Test("System- und Nice-Zeit zählen als Beschäftigung mit") func systemAndNiceCount() { let before = CPUTicks(user: 0, system: 0, idle: 0, nice: 0) let after = CPUTicks(user: 25, system: 25, idle: 50, nice: 0) #expect(CPUTicks.usage(from: before, to: after) == 0.5) } @Test("Zwei identische Abtastungen ergeben null statt einer Division durch null") func identicalSamples() { let ticks = CPUTicks(user: 10, system: 10, idle: 10, nice: 10) #expect(CPUTicks.usage(from: ticks, to: ticks) == 0) } @Test("Ein Zählerüberlauf ergibt keinen Ausreißer") func counterWraparound() { // 32-Bit-Zähler laufen nach gut 400 Tagen bei 100 Hz über. Rechnet man // naiv, springt die Anzeige einmalig auf einen absurden Wert. let before = CPUTicks(user: .max - 10, system: 0, idle: .max - 10, nice: 0) let after = CPUTicks(user: 10, system: 0, idle: 10, nice: 0) let usage = CPUUsageRange.contains(CPUTicks.usage(from: before, to: after)) #expect(usage) } @Test("Das Ergebnis liegt immer zwischen 0 und 1") func alwaysNormalised() { for _ in 0..<50 { let before = CPUTicks(user: .random(in: 0...100_000), system: .random(in: 0...100_000), idle: .random(in: 0...100_000), nice: .random(in: 0...100_000)) let after = CPUTicks(user: .random(in: 0...100_000), system: .random(in: 0...100_000), idle: .random(in: 0...100_000), nice: .random(in: 0...100_000)) #expect(CPUUsageRange.contains(CPUTicks.usage(from: before, to: after))) } } } private let CPUUsageRange = 0.0...1.0 @Suite("Speicherdruck") struct MemoryPressureTests { @Test("Viel frei bedeutet grüner Bereich") func plentyFree() { #expect(MemoryPressure.classify(free: 0.5, compressed: 0.05, swapUsed: 0) == .normal) } @Test("Wenig frei und viel komprimiert bedeutet Warnbereich") func compressedMeansWarning() { #expect(MemoryPressure.classify(free: 0.08, compressed: 0.25, swapUsed: 0) == .warning) } @Test("Aktive Auslagerung ist immer kritisch, egal wie viel frei scheint") func swapIsAlwaysCritical() { // Sobald ausgelagert wird, bremst der Rechner spürbar — auch wenn die // Freispeicheranzeige noch harmlos aussieht. #expect(MemoryPressure.classify(free: 0.4, compressed: 0.01, swapUsed: 2 * 1024 * 1024 * 1024) == .critical) } @Test("Die Grenzwerte springen nicht bei winzigen Änderungen hin und her") func classificationIsStable() { let a = MemoryPressure.classify(free: 0.30, compressed: 0.10, swapUsed: 0) let b = MemoryPressure.classify(free: 0.301, compressed: 0.101, swapUsed: 0) #expect(a == b) } } @Suite("Durchsatz") struct ThroughputTests { @Test("Tausend Byte in einer Sekunde sind tausend Byte pro Sekunde") func simpleRate() { #expect(Throughput.rate(previous: 0, current: 1000, elapsed: 1) == 1000) } @Test("Die Rate hängt nicht von der Abtastrate ab") func rateIsIndependentOfInterval() { // Der eigentliche Zweck: das Menüleisten-Modul tastet je nach // Einstellung alle 1 bis 10 Sekunden ab. Ohne Normierung zeigte es bei // 5 Sekunden das Fünffache an. let oneSecond = Throughput.rate(previous: 0, current: 1000, elapsed: 1) let fiveSeconds = Throughput.rate(previous: 0, current: 5000, elapsed: 5) #expect(oneSecond == fiveSeconds) } @Test("Ein Zählerüberlauf ergibt keinen Ausreißer") func wraparoundIsHandled() { let rate = Throughput.rate(previous: UInt64(UInt32.max) - 100, current: 100, elapsed: 1) #expect(rate >= 0) #expect(rate < 1_000_000) } @Test("Ein zurückgesetzter Zähler ergibt null statt eines Sprungs") func counterResetGivesZero() { // Beim Interface-Wechsel — WLAN aus, VPN an — fangen die Zähler wieder // bei null an. Ein naiver Vergleich zeigte dann einen riesigen Wert. #expect(Throughput.rate(previous: 5_000_000, current: 1000, elapsed: 1) == 0) } @Test("Kein vergangenes Intervall ergibt null statt einer Division durch null") func zeroElapsed() { #expect(Throughput.rate(previous: 0, current: 1000, elapsed: 0) == 0) } @Test("Ein negatives Intervall wird verworfen") func negativeElapsed() { // Kann bei einer Zeitumstellung vorkommen. #expect(Throughput.rate(previous: 0, current: 1000, elapsed: -5) == 0) } } @Suite("Zahlenformat") struct FormattingTests { @Test("Byteraten wechseln die Einheit statt fünfstellig zu werden") func rateUsesSensibleUnits() { #expect(Throughput.formatted(0).contains("0")) #expect(Throughput.formatted(2_400_000).contains("MB")) #expect(Throughput.formatted(42_000).contains("KB")) } @Test("Die Rate trägt immer die Zeiteinheit — sonst liest man sie als Menge") func rateAlwaysCarriesPerSecond() { for value in [0.0, 500, 42_000, 2_400_000, 900_000_000] { #expect(Throughput.formatted(value).hasSuffix("/s")) } } @Test("Prozentwerte sind ganzzahlig") func percentIsWhole() { // Nachkommastellen bei der CPU-Auslastung täuschen eine Genauigkeit // vor, die die Messung nicht hat — und sie zappeln im Sekundentakt. #expect(MetricFormat.percent(0.4567) == "46 %") #expect(MetricFormat.percent(0) == "0 %") #expect(MetricFormat.percent(1) == "100 %") } @Test("Temperaturen bekommen ein Gradzeichen und keine Nachkommastelle") func temperatureFormat() { #expect(MetricFormat.temperature(46.7) == "47°") } }