Pulse/internal/hostmetrics/coverage_additional_test.go
2026-03-18 16:06:30 +00:00

310 lines
9.9 KiB
Go

package hostmetrics
import (
"context"
"errors"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"time"
godisk "github.com/shirou/gopsutil/v4/disk"
)
func TestCollectCPUUsageBranches(t *testing.T) {
origCPUPercent := cpuPercent
t.Cleanup(func() {
cpuPercent = origCPUPercent
})
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
}