Files
pulse/internal/hostmetrics/coverage_additional_test.go
T
courtmanr@gmail.com 07a0d84464 Average host CPU usage across the report interval (#1648)
collectCPUUsage took a blocking 1-second spot sample once per report
interval, which overstates CPU on mostly-idle guests (reported on a
2-vCPU aarch64 KVM guest) and delayed every Collect() by a second.
Track the previous cumulative cpu.Times sample and report busy/total
across the elapsed interval instead, falling back to the spot sample
only for the first collection and counter resets (reboot, migration).
CPUUsagePercent keeps its meaning in the report schema.

Contract-Neutral: hostmetrics sampling behavior change with no subsystem contract surface delta
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 22:49:08 +01:00

320 lines
10 KiB
Go

package hostmetrics
import (
"context"
"errors"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"time"
gocpu "github.com/shirou/gopsutil/v4/cpu"
godisk "github.com/shirou/gopsutil/v4/disk"
)
func TestCollectCPUUsageBranches(t *testing.T) {
origCPUPercent := cpuPercent
origCPUTimes := cpuTimes
t.Cleanup(func() {
cpuPercent = origCPUPercent
cpuTimes = origCPUTimes
})
// Force the spot-sample fallback so the branches below stay deterministic
// regardless of the cross-interval delta state (issue #1648).
cpuTimes = func(ctx context.Context, percpu bool) ([]gocpu.TimesStat, error) {
return nil, errors.New("times unavailable")
}
cpuPercent = func(ctx context.Context, interval time.Duration, percpu bool) ([]float64, error) {
return nil, errors.New("boom")
}
if _, err := collectCPUUsage(context.Background()); err == nil {
t.Fatal("expected collectCPUUsage to return error when cpuPercent fails")
}
cpuPercent = func(ctx context.Context, interval time.Duration, percpu bool) ([]float64, error) {
return []float64{}, nil
}
usage, err := collectCPUUsage(context.Background())
if err != nil {
t.Fatalf("collectCPUUsage unexpected error for empty percentages: %v", err)
}
if usage != 0 {
t.Fatalf("expected empty percentages to return 0, got %v", usage)
}
cpuPercent = func(ctx context.Context, interval time.Duration, percpu bool) ([]float64, error) {
return []float64{-4.2}, nil
}
usage, err = collectCPUUsage(context.Background())
if err != nil {
t.Fatalf("collectCPUUsage unexpected error for negative value: %v", err)
}
if usage != 0 {
t.Fatalf("expected negative usage to clamp to 0, got %v", usage)
}
cpuPercent = func(ctx context.Context, interval time.Duration, percpu bool) ([]float64, error) {
return []float64{147.9}, nil
}
usage, err = collectCPUUsage(context.Background())
if err != nil {
t.Fatalf("collectCPUUsage unexpected error for >100 value: %v", err)
}
if usage != 100 {
t.Fatalf("expected >100 usage to clamp to 100, got %v", usage)
}
}
func TestCollectDisksBranchCoverage(t *testing.T) {
origPartitions := diskPartitions
origUsage := diskUsage
origQueryZpoolStats := queryZpoolStats
t.Cleanup(func() {
diskPartitions = origPartitions
diskUsage = origUsage
queryZpoolStats = origQueryZpoolStats
})
diskPartitions = func(ctx context.Context, all bool) ([]godisk.PartitionStat, error) {
return []godisk.PartitionStat{
{Device: "/dev/ignore", Mountpoint: "", Fstype: "ext4"},
{Device: "/dev/dup-a", Mountpoint: "/dup", Fstype: "ext4"},
{Device: "/dev/dup-b", Mountpoint: "/dup", Fstype: "ext4"},
{Device: "/dev/excluded", Mountpoint: "/skip/me", Fstype: "ext4"},
{Device: "/dev/err", Mountpoint: "/err", Fstype: "ext4"},
{Device: "/dev/zero", Mountpoint: "/zero", Fstype: "ext4"},
{Device: "/dev/tmp", Mountpoint: "/tmpfs", Fstype: "tmpfs"},
{Device: "", Mountpoint: "/zfs-empty", Fstype: "zfs"},
{Device: "tank/root", Mountpoint: "/zfs", Fstype: "zfs"},
{Device: "/dev/data", Mountpoint: "/data", Fstype: "ext4"},
}, nil
}
diskUsage = func(ctx context.Context, path string) (*godisk.UsageStat, error) {
switch path {
case "/dup":
return &godisk.UsageStat{Total: 10, Used: 4, Free: 6, UsedPercent: 40}, nil
case "/skip/me":
return &godisk.UsageStat{Total: 100, Used: 10, Free: 90, UsedPercent: 10}, nil
case "/err":
return nil, errors.New("usage failure")
case "/zero":
return &godisk.UsageStat{Total: 0, Used: 0, Free: 0, UsedPercent: 0}, nil
case "/tmpfs":
return &godisk.UsageStat{Total: 100, Used: 100, Free: 0, UsedPercent: 100}, nil
case "/zfs-empty":
return &godisk.UsageStat{Total: 100, Used: 20, Free: 80, UsedPercent: 20}, nil
case "/zfs":
return &godisk.UsageStat{Total: 100, Used: 30, Free: 70, UsedPercent: 30}, nil
case "/data":
return &godisk.UsageStat{Total: 200, Used: 50, Free: 150, UsedPercent: 25}, nil
default:
return nil, errors.New("unexpected mountpoint")
}
}
queryZpoolStats = func(ctx context.Context, pools []string) (map[string]zpoolStats, error) {
return map[string]zpoolStats{
"tank": {Size: 100, Alloc: 30, Free: 70},
}, nil
}
disks := collectDisks(context.Background(), []string{"/skip/*"})
if len(disks) != 3 {
t.Fatalf("expected 3 disks, got %d: %+v", len(disks), disks)
}
byMount := make(map[string]bool, len(disks))
for _, disk := range disks {
byMount[disk.Mountpoint] = true
}
for _, expected := range []string{"/data", "/dup", "/zfs"} {
if !byMount[expected] {
t.Fatalf("expected mountpoint %s in collected disks: %+v", expected, disks)
}
}
for _, skipped := range []string{"/skip/me", "/err", "/zero", "/tmpfs", "/zfs-empty"} {
if byMount[skipped] {
t.Fatalf("did not expect skipped mountpoint %s in collected disks: %+v", skipped, disks)
}
}
}
func TestCollectDisksPrefersShallowerMountpoint(t *testing.T) {
origPartitions := diskPartitions
origUsage := diskUsage
t.Cleanup(func() {
diskPartitions = origPartitions
diskUsage = origUsage
})
diskPartitions = func(ctx context.Context, all bool) ([]godisk.PartitionStat, error) {
return []godisk.PartitionStat{
{Device: "/dev/sdb1", Mountpoint: "/srv/data/sub", Fstype: "ext4"},
{Device: "/dev/sdb1", Mountpoint: "/srv/data", Fstype: "ext4"},
}, nil
}
diskUsage = func(ctx context.Context, path string) (*godisk.UsageStat, error) {
return &godisk.UsageStat{Total: 500, Used: 125, Free: 375, UsedPercent: 25}, nil
}
disks := collectDisks(context.Background(), nil)
if len(disks) != 1 {
t.Fatalf("expected 1 deduplicated disk, got %d: %+v", len(disks), disks)
}
if disks[0].Mountpoint != "/srv/data" {
t.Fatalf("expected shallower mountpoint /srv/data, got %s", disks[0].Mountpoint)
}
}
func TestCollectDiskIOFiltersAndError(t *testing.T) {
origDiskIOCounters := diskIOCounters
t.Cleanup(func() {
diskIOCounters = origDiskIOCounters
})
diskIOCounters = func(ctx context.Context, names ...string) (map[string]godisk.IOCountersStat, error) {
return map[string]godisk.IOCountersStat{
"sda": {ReadBytes: 1},
"sda1": {ReadBytes: 2},
"nvme0n1": {ReadBytes: 3},
"nvme0n1p1": {ReadBytes: 4},
"xvda10": {ReadBytes: 5},
"loop0": {ReadBytes: 6},
"ram0": {ReadBytes: 7},
"dm-0": {ReadBytes: 8},
"sdb": {ReadBytes: 9},
"md0": {ReadBytes: 10},
}, nil
}
disks := collectDiskIO(context.Background(), []string{"sdb"})
if len(disks) != 3 {
t.Fatalf("expected 3 disk I/O entries after filtering, got %d: %+v", len(disks), disks)
}
gotDevices := make([]string, 0, len(disks))
for _, disk := range disks {
gotDevices = append(gotDevices, disk.Device)
}
wantDevices := []string{"md0", "nvme0n1", "sda"}
if !reflect.DeepEqual(gotDevices, wantDevices) {
t.Fatalf("unexpected disk I/O devices: got %v, want %v", gotDevices, wantDevices)
}
diskIOCounters = func(ctx context.Context, names ...string) (map[string]godisk.IOCountersStat, error) {
return nil, errors.New("io unavailable")
}
if got := collectDiskIO(context.Background(), nil); got != nil {
t.Fatalf("expected nil when diskIOCounters errors, got %+v", got)
}
}
func TestIsPartitionPatterns(t *testing.T) {
tests := []struct {
name string
device string
want bool
}{
{name: "traditional single digit", device: "sda1", want: true},
{name: "traditional multi digit", device: "sda10", want: true},
{name: "virtio partition", device: "vda2", want: true},
{name: "xen partition", device: "xvda12", want: true},
{name: "nvme partition", device: "nvme0n1p1", want: true},
{name: "zfs zd partition", device: "zd16p1", want: true},
{name: "md whole device", device: "md0", want: false},
{name: "loop device", device: "loop0", want: false},
{name: "nvme whole device", device: "nvme0n1", want: false},
{name: "traditional whole disk", device: "sda", want: false},
{name: "zfs whole zd", device: "zd0", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isPartition(tt.device)
if got != tt.want {
t.Fatalf("isPartition(%q) = %v, want %v", tt.device, got, tt.want)
}
})
}
}
func TestFetchZpoolStats(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("shell script based zpool mock is unix-only")
}
t.Run("empty pools", func(t *testing.T) {
stats, err := fetchZpoolStats(context.Background(), nil)
if err != nil {
t.Fatalf("fetchZpoolStats unexpected error for empty pools: %v", err)
}
if stats != nil {
t.Fatalf("expected nil stats for empty pools, got %+v", stats)
}
})
t.Run("success", func(t *testing.T) {
dir := writeFakeZpool(t, "#!/bin/sh\nprintf \"tank\\t100\\t40\\t60\\n\"\nprintf \"bpool\\t200\\t80\\t120\\n\"\n")
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
stats, err := fetchZpoolStats(context.Background(), []string{"tank", "bpool"})
if err != nil {
t.Fatalf("fetchZpoolStats unexpected error: %v", err)
}
want := map[string]zpoolStats{
"tank": {Size: 100, Alloc: 40, Free: 60},
"bpool": {Size: 200, Alloc: 80, Free: 120},
}
if !reflect.DeepEqual(stats, want) {
t.Fatalf("fetchZpoolStats mismatch: got %+v, want %+v", stats, want)
}
})
t.Run("command error", func(t *testing.T) {
dir := writeFakeZpool(t, "#!/bin/sh\necho \"boom\" 1>&2\nexit 7\n")
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
stats, err := fetchZpoolStats(context.Background(), []string{"tank"})
if err == nil {
t.Fatal("expected fetchZpoolStats to fail when zpool command exits non-zero")
}
if stats != nil {
t.Fatalf("expected nil stats on command failure, got %+v", stats)
}
})
t.Run("parse error", func(t *testing.T) {
dir := writeFakeZpool(t, "#!/bin/sh\nprintf \"not\\tusable\\tdata\\n\"\n")
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
stats, err := fetchZpoolStats(context.Background(), []string{"tank"})
if err == nil {
t.Fatal("expected parse error for unusable zpool output")
}
if !strings.Contains(err.Error(), "no usable data") {
t.Fatalf("expected no usable data error, got %v", err)
}
if stats != nil {
t.Fatalf("expected nil stats on parse failure, got %+v", stats)
}
})
}
func writeFakeZpool(t *testing.T, script string) string {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "zpool")
if err := os.WriteFile(path, []byte(script), 0o700); err != nil {
t.Fatalf("failed to write fake zpool binary: %v", err)
}
return dir
}