Files
warmbly/internal/pkg/nodeagent/usage_test.go
T
Matthew MeszarosandDevin AI f56278488f feat: keep worker farewell heartbeats independent of IP discovery and identify CPU-set resource scope accurately
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-05 11:43:16 +00:00

143 lines
5.1 KiB
Go

package nodeagent
import (
"os"
"runtime"
"strconv"
"testing"
"time"
"github.com/warmbly/warmbly/internal/models"
)
func samplerWithFiles(files map[string]string) *usageSampler {
return &usageSampler{readFile: func(path string) ([]byte, error) {
if value, ok := files[path]; ok {
return []byte(value), nil
}
return nil, os.ErrNotExist
}}
}
func TestHostUsageSeparatesResidentMemoryAndMachineMemory(t *testing.T) {
files := map[string]string{
"/proc/self/status": "VmRSS: 140288 kB\n",
"/proc/meminfo": "MemTotal: 8388608 kB\nMemAvailable: 6291456 kB\n",
"/proc/stat": "cpu 100 0 100 800 0 0 0 0 50 0\n",
}
s := samplerWithFiles(files)
var first models.NodeUsage
s.sample(&first)
if first.CPUPercent != nil || *first.ResidentMB != 137 || *first.MemoryUsedMB != 2048 || *first.MemoryLimitMB != 8192 || first.MemoryScope != "host" {
t.Fatalf("unexpected initial snapshot: %+v", first)
}
files["/proc/stat"] = "cpu 125 0 125 850 0 0 0 0 100 0\n"
var next models.NodeUsage
s.sample(&next)
if next.CPUScope != "host" || next.CPUPercent == nil || *next.CPUPercent != 50 {
t.Fatalf("expected 50%% host CPU excluding guest double counting: %+v", next)
}
}
func TestContainerUsageUsesLimitsAndCPUInterval(t *testing.T) {
files := map[string]string{
"/proc/meminfo": "MemTotal: 8388608 kB\nMemAvailable: 6291456 kB\n",
"/sys/fs/cgroup/memory.current": "143654912",
"/sys/fs/cgroup/memory.max": "536870912",
"/sys/fs/cgroup/cpu.stat": "usage_usec 1250000\n",
"/sys/fs/cgroup/cpu.max": "50000 100000",
}
s := samplerWithFiles(files)
s.previous = cpuReading{scope: "container", used: 1000000, cores: 0.5, at: time.Now().Add(-time.Second)}
var usage models.NodeUsage
s.sample(&usage)
if usage.MemoryScope != "container" || *usage.MemoryUsedMB != 137 || *usage.MemoryLimitMB != 512 || usage.CPUPercent == nil || *usage.CPUPercent < 45 || *usage.CPUPercent > 51 {
t.Fatalf("expected container 137/512 MiB, approximately 50%% CPU: %+v", usage)
}
files["/sys/fs/cgroup/cpu.max"] = "25000 100000"
var changed models.NodeUsage
s.sample(&changed)
if changed.CPUPercent != nil {
t.Fatal("a changed CPU quota must establish a new baseline")
}
}
func TestMissingTelemetryIsNotZero(t *testing.T) {
s := samplerWithFiles(nil)
var usage models.NodeUsage
s.sample(&usage)
if usage.CPUPercent != nil || usage.ResidentMB != nil || usage.MemoryUsedMB != nil || usage.MemoryLimitMB != nil {
t.Fatalf("missing OS metrics must remain unavailable: %+v", usage)
}
}
func TestCgroupV1AndNestedV2Memory(t *testing.T) {
for _, version := range []string{"v1", "v2"} {
t.Run(version, func(t *testing.T) {
files := map[string]string{"/proc/meminfo": "MemTotal: 8388608 kB\nMemAvailable: 6291456 kB\n"}
if version == "v1" {
files["/sys/fs/cgroup/memory/memory.usage_in_bytes"] = "143654912"
files["/sys/fs/cgroup/memory/memory.limit_in_bytes"] = "536870912"
} else {
files["/proc/self/cgroup"] = "0::/worker\n"
files["/sys/fs/cgroup/worker/cgroup.procs"] = "1\n"
files["/sys/fs/cgroup/worker/memory.current"] = "143654912"
files["/sys/fs/cgroup/worker/memory.max"] = "536870912"
}
var usage models.NodeUsage
samplerWithFiles(files).sample(&usage)
if usage.MemoryScope != "container" || *usage.MemoryLimitMB != 512 {
t.Fatalf("unexpected %s memory snapshot: %+v", version, usage)
}
})
}
}
func TestCPUSetSize(t *testing.T) {
for raw, want := range map[string]int{"0-3,6,8-9": 7, "0": 1, "": 0, "3-1": 0, "no": 0} {
if got := cpuSetSize(raw); got != want {
t.Errorf("cpuSetSize(%q) = %d, want %d", raw, got, want)
}
}
}
func TestContainerMemoryDoesNotRequireHostMemory(t *testing.T) {
files := map[string]string{
"/sys/fs/cgroup/memory.current": "143654912",
"/sys/fs/cgroup/memory.max": "536870912",
}
var usage models.NodeUsage
samplerWithFiles(files).sample(&usage)
if usage.MemoryScope != "container" || usage.MemoryUsedMB == nil || *usage.MemoryUsedMB != 137 || *usage.MemoryLimitMB != 512 {
t.Fatalf("expected cgroup memory even without host totals: %+v", usage)
}
}
func TestContainerCPUQuotaEqualToAvailableCPUs(t *testing.T) {
files := map[string]string{
"/sys/fs/cgroup/cpu.stat": "usage_usec 1250000\n",
"/sys/fs/cgroup/cpu.max": strconv.Itoa(runtime.NumCPU()*100000) + " 100000",
}
var usage models.NodeUsage
samplerWithFiles(files).sample(&usage)
if usage.CPUScope != "container" {
t.Fatalf("a finite CPU quota should retain container scope: %+v", usage)
}
}
func TestContainerCPUSetRecognizesHostCPUsOutsideProcessAffinity(t *testing.T) {
procStat := "cpu 100 0 100 800 0 0 0 0\n"
for i := 0; i <= runtime.NumCPU(); i++ {
procStat += "cpu" + strconv.Itoa(i) + " 1 0 1 8 0 0 0 0\n"
}
files := map[string]string{
"/proc/stat": procStat,
"/sys/fs/cgroup/cpu.stat": "usage_usec 1250000\n",
"/sys/fs/cgroup/cpu.max": "max 100000",
"/sys/fs/cgroup/cpuset.cpus.effective": "0-" + strconv.Itoa(runtime.NumCPU()-1),
}
if got := samplerWithFiles(files).cpu(); got.scope != "container" || got.cores != float64(runtime.NumCPU()) {
t.Fatalf("a cpuset smaller than the host must use container scope: %+v", got)
}
}