diff --git a/internal/animate/ease.go b/internal/animate/ease.go index b558773..6c28b93 100644 --- a/internal/animate/ease.go +++ b/internal/animate/ease.go @@ -30,11 +30,30 @@ func Clamp01(t float64) float64 { } func Spring(current, target float64, velocity *float64, dt float64) float64 { + if math.IsNaN(target) || math.IsInf(target, 0) { + target = 0 + } + if math.IsNaN(current) || math.IsInf(current, 0) { + current = target + } + if velocity == nil { + return target + } + if math.IsNaN(*velocity) || math.IsInf(*velocity, 0) { + *velocity = 0 + } + if dt <= 0 || math.IsNaN(dt) || math.IsInf(dt, 0) { + return current + } force := stiffness * (target - current) *velocity += force * dt *velocity *= math.Exp(-damping * dt) if *velocity < 0.0001 && *velocity > -0.0001 { *velocity = 0 } - return current + *velocity*dt + res := current + *velocity*dt + if math.IsNaN(res) || math.IsInf(res, 0) { + return target + } + return res } diff --git a/internal/animate/ease_test.go b/internal/animate/ease_test.go index 6cd77df..5e05baf 100644 --- a/internal/animate/ease_test.go +++ b/internal/animate/ease_test.go @@ -67,3 +67,31 @@ func TestSpringStableAtUITickInterval(t *testing.T) { t.Errorf("Spring oscillated negative at dt=0.13 (min %f) — value would render as 0 B/s", minVal) } } + +func TestSpringNaNAndInfGuards(t *testing.T) { + var vel float64 + // Target is NaN + val := Spring(100, math.NaN(), &vel, 0.13) + if math.IsNaN(val) || math.IsInf(val, 0) { + t.Errorf("Spring with NaN target returned %f", val) + } + + // Current is NaN + val = Spring(math.NaN(), 100, &vel, 0.13) + if math.IsNaN(val) || math.IsInf(val, 0) { + t.Errorf("Spring with NaN current returned %f", val) + } + + // Velocity is NaN + vel = math.NaN() + val = Spring(100, 100, &vel, 0.13) + if math.IsNaN(val) || math.IsInf(val, 0) || math.IsNaN(vel) { + t.Errorf("Spring with NaN velocity returned val=%f, vel=%f", val, vel) + } + + // dt is NaN + val = Spring(100, 100, &vel, math.NaN()) + if math.IsNaN(val) || math.IsInf(val, 0) { + t.Errorf("Spring with NaN dt returned %f", val) + } +} diff --git a/internal/history/persist_test.go b/internal/history/persist_test.go index da4f07f..bb4a08b 100644 --- a/internal/history/persist_test.go +++ b/internal/history/persist_test.go @@ -35,6 +35,13 @@ func TestSaveLoad(t *testing.T) { } func TestLoadMissing(t *testing.T) { + dir := t.TempDir() + orig := statsPath + statsPath = func() (string, error) { + return filepath.Join(dir, "nonexistent_stats.json"), nil + } + defer func() { statsPath = orig }() + tracker := NewTracker() err := tracker.Load() if err == nil { diff --git a/internal/ui/model.go b/internal/ui/model.go index 608470a..1d46440 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -212,7 +212,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case pingMsg: m.pingLatency = time.Duration(msg) - return m, nil + return m, m.pingTick() case sampleMsg: if msg.Err != nil { @@ -502,7 +502,7 @@ func FormatBps(bps float64, unit UnitMode) string { } func FormatBpsExt(bps float64, unit UnitMode, bits bool) string { - if bps < 0 { + if bps < 0 || math.IsNaN(bps) || math.IsInf(bps, 0) { bps = 0 } if bits { diff --git a/internal/ui/model_test.go b/internal/ui/model_test.go index 12b7d8a..256b23d 100644 --- a/internal/ui/model_test.go +++ b/internal/ui/model_test.go @@ -42,10 +42,15 @@ func TestFormatBpsExt(t *testing.T) { } func TestFormatBpsExt_EdgeCases(t *testing.T) { - // Test NaN and Inf - should not crash - _ = FormatBpsExt(math.NaN(), UnitAuto, false) - _ = FormatBpsExt(math.Inf(1), UnitAuto, false) - _ = FormatBpsExt(math.Inf(-1), UnitAuto, false) + if got := FormatBpsExt(math.NaN(), UnitAuto, false); got != "0 B/s" { + t.Errorf("FormatBpsExt(NaN) = %q; want '0 B/s'", got) + } + if got := FormatBpsExt(math.Inf(1), UnitAuto, false); got != "0 B/s" { + t.Errorf("FormatBpsExt(+Inf) = %q; want '0 B/s'", got) + } + if got := FormatBpsExt(math.Inf(-1), UnitAuto, false); got != "0 B/s" { + t.Errorf("FormatBpsExt(-Inf) = %q; want '0 B/s'", got) + } } func TestFormatBpsFixedWidth(t *testing.T) { diff --git a/internal/ui/views.go b/internal/ui/views.go index f113ebd..08d851d 100644 --- a/internal/ui/views.go +++ b/internal/ui/views.go @@ -30,6 +30,15 @@ func max(a, b int) int { } func maxf(a, b float64) float64 { + if math.IsNaN(a) { + if math.IsNaN(b) { + return 0 + } + return b + } + if math.IsNaN(b) { + return a + } if a > b { return a } @@ -96,6 +105,9 @@ func formatInterval(d time.Duration) string { } func formatBytes(b float64) string { + if b < 0 || math.IsNaN(b) || math.IsInf(b, 0) { + b = 0 + } const ( KB = 1024.0 MB = 1024 * KB