package platform import ( "os" "path/filepath" "reflect" "testing" "time" ) // resetPeakStore points a store at a fresh temp file and clears its cache for // the duration of the test. func resetPeakStore(t *testing.T, s *observedPeakStore) { t.Helper() old := *s s.path = filepath.Join(t.TempDir(), "peaks.json") s.loaded = false s.peaks = nil s.candidates = nil t.Cleanup(func() { *s = old }) } func TestResolveFanMaxRPM(t *testing.T) { resetPeakStore(t, fanPeaks) // No persisted file yet: unknown fans fall back to their own current RPM. got := ResolveFanMaxRPM(map[string]float64{"A": 4000, "B": 9000}) if !reflect.DeepEqual(got, map[string]float64{"A": 4000, "B": 9000}) { t.Fatalf("no-persist fallback: got %v", got) } if err := os.WriteFile(fanPeaks.path, []byte(`{"max_rpm":{"A":17000}}`), 0644); err != nil { t.Fatal(err) } got = ResolveFanMaxRPM(map[string]float64{"A": 4000, "B": 9000, "C": 5000}) // A: persisted peak. B/C: no own entry -> largest peer peak (A's 17000). if want := map[string]float64{"A": 17000, "B": 17000, "C": 17000}; !reflect.DeepEqual(got, want) { t.Fatalf("peer fallback: got %v want %v", got, want) } } func TestObservedPeakStore(t *testing.T) { s := &observedPeakStore{ path: filepath.Join(t.TempDir(), "peaks.json"), jsonKey: "max_w", roundUp: 50, minHold: time.Second, } t0 := time.Unix(0, 0) // A single spike does not stick. s.observe(map[string]float64{"0": 900}, t0) if len(s.snapshot()) != 0 { t.Fatalf("transient spike should not persist: %v", s.snapshot()) } // Held past minHold → sticks, rounded up to the next 50. s.observe(map[string]float64{"0": 920}, t0.Add(1200*time.Millisecond)) if got := s.snapshot()["0"]; got != 950 { t.Fatalf("held peak: got %v want 950", got) } // A lower reading never lowers the peak. s.observe(map[string]float64{"0": 400}, t0.Add(5*time.Second)) if got := s.snapshot()["0"]; got != 950 { t.Fatalf("peak must not drop: got %v", got) } // Fresh store reloads from disk. s2 := &observedPeakStore{path: s.path, jsonKey: "max_w"} if got := s2.snapshot()["0"]; got != 950 { t.Fatalf("reload from disk: got %v want 950", got) } } func TestUpdatePSUObservationKeyedByOrdinal(t *testing.T) { resetPeakStore(t, psuPeaks) now := time.Unix(0, 0) psus := []PSUReading{{Name: "PSU1", PowerW: 1200}, {Name: "PSU2", PowerW: 1400}} updatePSUObservation(psus, now) updatePSUObservation(psus, now.Add(1200*time.Millisecond)) got := ObservedPSUMaxW() if got["0"] != 1200 || got["1"] != 1400 { t.Fatalf("keyed-by-ordinal peaks: got %v", got) } } func TestApplyFanCheckDefaults(t *testing.T) { var o FanCheckOptions applyFanCheckDefaults(&o) if o.PlateauHoldSec != 60 || o.PlateauDeltaRPM != 50 || o.MinLoadSec != 90 || o.MaxLoadSec != 900 || o.RampConfirmRPM != 150 { t.Fatalf("unexpected defaults: %+v", o) } o = FanCheckOptions{PlateauHoldSec: 120, MinLoadSec: 30, MaxLoadSec: 40} applyFanCheckDefaults(&o) if o.MinLoadSec < o.PlateauHoldSec { t.Fatalf("MinLoadSec must be >= PlateauHoldSec, got %d", o.MinLoadSec) } if o.MaxLoadSec <= o.MinLoadSec { t.Fatalf("MaxLoadSec must exceed MinLoadSec, got %d", o.MaxLoadSec) } } func TestSanitizeSummaryKey(t *testing.T) { for in, want := range map[string]string{ "F2U-1": "F2U-1", "aspeed / fan1": "aspeed___fan1", "CPU0_DIMM": "CPU0_DIMM", "weird=key here": "weird_key_here", } { if got := sanitizeSummaryKey(in); got != want { t.Errorf("sanitizeSummaryKey(%q)=%q want %q", in, got, want) } } } func TestParseFanSpeeds(t *testing.T) { raw := "FAN1 | 2400.000 | RPM | ok\nFAN2 | 1800 RPM | ok | ok\nFAN3 | na | RPM | ns\n" got := parseFanSpeeds(raw) if len(got) != 2 { t.Fatalf("fans=%d want 2 (%v)", len(got), got) } if got[0].Name != "FAN1" || got[0].RPM != 2400 { t.Fatalf("fan0=%+v", got[0]) } if got[1].Name != "FAN2" || got[1].RPM != 1800 { t.Fatalf("fan1=%+v", got[1]) } } func TestFirstFanInputValue(t *testing.T) { feature := map[string]any{ "fan1_input": 9200.0, } got, ok := firstFanInputValue(feature) if !ok || got != 9200 { t.Fatalf("got=%v ok=%v", got, ok) } } func TestParseFanDutyCyclePctSensorsJSON(t *testing.T) { raw := []byte(`{ "chip0": { "fan1": {"input": 9000}, "pwm1": {"input": 128}, "pwm1_enable": {"input": 1} }, "chip1": { "pwm2": {"input": 64} } }`) got, ok := parseFanDutyCyclePctSensorsJSON(raw) if !ok { t.Fatalf("expected duty cycle telemetry to be parsed") } if got < 57 || got > 58 { t.Fatalf("got=%v want ~57.1", got) } } func TestEstimateFanDutyCyclePctFromObservation(t *testing.T) { resetPeakStore(t, fanPeaks) start := time.Unix(100, 0) updateFanObservation([]FanReading{{Name: "FAN1", RPM: 5000}}, start) if _, ok := estimateFanDutyCyclePctFromObservation([]FanReading{{Name: "FAN1", RPM: 2500}}); ok { t.Fatalf("single-sample spike should not establish observed max") } updateFanObservation([]FanReading{{Name: "FAN1", RPM: 5200}}, start.Add(500*time.Millisecond)) updateFanObservation([]FanReading{{Name: "FAN1", RPM: 5100}}, start.Add(1500*time.Millisecond)) got, ok := estimateFanDutyCyclePctFromObservation([]FanReading{{Name: "FAN1", RPM: 2600}}) if !ok { t.Fatalf("expected estimated duty cycle from persisted observed max") } if got < 43 || got > 44 { t.Fatalf("got=%v want ~43.3", got) } fanPeaks.loaded = false fanPeaks.peaks = nil fanPeaks.candidates = nil got, ok = estimateFanDutyCyclePctFromObservation([]FanReading{{Name: "FAN1", RPM: 2600}}) if !ok { t.Fatalf("expected persisted observed max to be reloaded from disk") } if got < 43 || got > 44 { t.Fatalf("reloaded got=%v want ~43.3", got) } } func TestParseDCMIPowerReading(t *testing.T) { raw := ` Instantaneous power reading: 512 Watts Minimum during sampling period: 498 Watts ` if got := parseDCMIPowerReading(raw); got != 512 { t.Fatalf("parseDCMIPowerReading()=%v want 512", got) } } func TestEffectiveSystemPowerReading(t *testing.T) { now := time.Now() cache := cachedPowerReading{Value: 480, UpdatedAt: now.Add(-5 * time.Second)} got, updated := effectiveSystemPowerReading(cache, 0, "", "", "", now) if got != 480 { t.Fatalf("got=%v want cached 480", got) } if updated.Value != 480 { t.Fatalf("updated=%+v", updated) } got, updated = effectiveSystemPowerReading(cache, 530, "dcmi", "fallback", "test", now) if got != 530 { t.Fatalf("got=%v want 530", got) } if updated.Value != 530 { t.Fatalf("updated=%+v", updated) } expired := cachedPowerReading{Value: 480, UpdatedAt: now.Add(-systemPowerHoldTTL - time.Second)} got, _ = effectiveSystemPowerReading(expired, 0, "", "", "", now) if got != 0 { t.Fatalf("expired cache returned %v want 0", got) } }