Files
beszel-ipv6/agent/system_test.go
henrygd d40372842b fix(agent): guard against unsigned underflow in memory calculations (closes #1978)
Extract host memory derivation into calculateHostMemoryUsage and use
saturating subtraction to prevent uint64 underflow caused by race
conditions when reading /proc/meminfo. Also ensure UsedPercent guards
against division by zero.

Co-authored-by: Sven van Ginkel <svenvanginkel@icloud.com>
2026-08-13 17:06:10 -04:00

116 lines
3.6 KiB
Go

package agent
import (
"testing"
"github.com/henrygd/beszel/internal/common"
"github.com/henrygd/beszel/internal/entities/system"
"github.com/shirou/gopsutil/v4/mem"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGatherStatsDoesNotAttachDetailsToCachedRequests(t *testing.T) {
agent := &Agent{
cache: NewSystemDataCache(),
systemDetails: system.Details{Hostname: "updated-host", Podman: true},
detailsDirty: true,
}
cached := &system.CombinedData{
Info: system.Info{Hostname: "cached-host"},
}
agent.cache.Set(cached, defaultDataCacheTimeMs)
response := agent.gatherStats(common.DataRequestOptions{CacheTimeMs: defaultDataCacheTimeMs})
assert.Same(t, cached, response)
assert.Nil(t, response.Details)
assert.True(t, agent.detailsDirty)
assert.Equal(t, "cached-host", response.Info.Hostname)
assert.Nil(t, cached.Details)
secondResponse := agent.gatherStats(common.DataRequestOptions{CacheTimeMs: defaultDataCacheTimeMs})
assert.Same(t, cached, secondResponse)
assert.Nil(t, secondResponse.Details)
}
func TestCalculateHostMemoryUsage(t *testing.T) {
tests := []struct {
name string
memory mem.VirtualMemoryStat
htop bool
used, cacheBuff, swapUsed uint64
}{
{
name: "normal",
memory: mem.VirtualMemoryStat{Total: 100, Available: 40, Used: 60, Free: 20, Cached: 25, Buffers: 10, Shared: 5, SwapTotal: 20, SwapFree: 8, SwapCached: 2},
used: 60,
cacheBuff: 30,
swapUsed: 10,
},
{
name: "inconsistent counters saturate",
memory: mem.VirtualMemoryStat{Total: 100, Available: 110, Used: ^uint64(0) - 9, Free: 90, Cached: 5, Buffers: 10, Shared: 20, SwapTotal: 10, SwapFree: 9, SwapCached: 2},
used: 0,
cacheBuff: 0,
swapUsed: 0,
},
{
name: "htop subtraction saturates",
memory: mem.VirtualMemoryStat{Total: 100, Available: 20, Used: 80, Free: 90, Cached: 20, Buffers: 5, SwapTotal: 30, SwapFree: 10, SwapCached: 5},
htop: true,
used: 0,
cacheBuff: 25,
swapUsed: 15,
},
{
name: "zero cache from shared cancellation does not fall back",
memory: mem.VirtualMemoryStat{Total: 100, Used: 60, Free: 10, Cached: 20, Buffers: 10, Shared: 30},
used: 60,
cacheBuff: 0,
},
{
name: "absent cache counters use fallback",
memory: mem.VirtualMemoryStat{Total: 100, Used: 60, Free: 10},
used: 60,
cacheBuff: 30,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
used, cacheBuff, swapUsed := calculateHostMemoryUsage(&tt.memory, tt.htop)
assert.Equal(t, tt.used, used)
assert.Equal(t, tt.cacheBuff, cacheBuff)
assert.Equal(t, tt.swapUsed, swapUsed)
})
}
}
func TestUpdateSystemDetailsMarksDetailsDirty(t *testing.T) {
agent := &Agent{}
agent.updateSystemDetails(func(details *system.Details) {
details.Hostname = "updated-host"
details.Podman = true
})
assert.True(t, agent.detailsDirty)
assert.Equal(t, "updated-host", agent.systemDetails.Hostname)
assert.True(t, agent.systemDetails.Podman)
original := &system.CombinedData{}
realTimeResponse := agent.attachSystemDetails(original, 1000, true)
assert.Same(t, original, realTimeResponse)
assert.Nil(t, realTimeResponse.Details)
assert.True(t, agent.detailsDirty)
response := agent.attachSystemDetails(original, defaultDataCacheTimeMs, false)
require.NotNil(t, response.Details)
assert.NotSame(t, original, response)
assert.Equal(t, "updated-host", response.Details.Hostname)
assert.True(t, response.Details.Podman)
assert.False(t, agent.detailsDirty)
assert.Nil(t, original.Details)
}