Pulse/internal/hostmetrics/collector_test.go
courtmanr@gmail.com 1945795d36 Fix ZFS storage reporting on TrueNAS SCALE (#718)
- Refactor collector to support mocking
- Fix ZFS detection to support 'fuse.zfs' and case-insensitivity
- Add regression tests for ZFS dataset deduplication
2025-11-22 23:53:39 +00:00

133 lines
4.2 KiB
Go

package hostmetrics
import (
"context"
"testing"
godisk "github.com/shirou/gopsutil/v4/disk"
"github.com/stretchr/testify/assert"
)
func TestCollectDisks_ZFS_Deduplication(t *testing.T) {
// Save original functions and restore after test
origDiskPartitions := diskPartitions
origDiskUsage := diskUsage
defer func() {
diskPartitions = origDiskPartitions
diskUsage = origDiskUsage
}()
// Mock partitions
diskPartitions = func(ctx context.Context, all bool) ([]godisk.PartitionStat, error) {
return []godisk.PartitionStat{
{Device: "tank/dataset1", Mountpoint: "/mnt/dataset1", Fstype: "zfs"},
{Device: "tank/dataset2", Mountpoint: "/mnt/dataset2", Fstype: "zfs"},
{Device: "tank/dataset3", Mountpoint: "/mnt/dataset3", Fstype: "zfs"},
}, nil
}
// Mock usage
diskUsage = func(ctx context.Context, path string) (*godisk.UsageStat, error) {
// All datasets on the same pool share the same free space
return &godisk.UsageStat{
Total: 1000,
Used: 500,
Free: 500,
UsedPercent: 50.0,
}, nil
}
// Mock zpool stats query to fail so we test the fallback logic (which uses the datasets)
// We can't easily mock queryZpoolStats because it's in zfs.go and not exported/variable-ized in the same way,
// but we can rely on the fact that `exec.LookPath("zpool")` will likely fail or `zpool list` will fail in the test environment.
// However, to be sure, we should probably mock it.
// But wait, `summarizeZFSPools` calls `queryZpoolStats`.
// `queryZpoolStats` is a variable in zfs.go! `var queryZpoolStats = fetchZpoolStats`
// Let's mock queryZpoolStats too.
origQueryZpoolStats := queryZpoolStats
defer func() { queryZpoolStats = origQueryZpoolStats }()
queryZpoolStats = func(ctx context.Context, pools []string) (map[string]zpoolStats, error) {
return nil, assert.AnError // Simulate failure
}
ctx := context.Background()
disks := collectDisks(ctx)
// Should return 1 disk (the pool), not 3
assert.Len(t, disks, 1)
assert.Equal(t, "tank", disks[0].Device)
assert.Equal(t, int64(1000), disks[0].TotalBytes)
}
func TestCollectDisks_ZFS_CaseInsensitive(t *testing.T) {
// Save original functions
origDiskPartitions := diskPartitions
origDiskUsage := diskUsage
defer func() {
diskPartitions = origDiskPartitions
diskUsage = origDiskUsage
}()
// Mock partitions with "ZFS" (uppercase)
diskPartitions = func(ctx context.Context, all bool) ([]godisk.PartitionStat, error) {
return []godisk.PartitionStat{
{Device: "tank/dataset1", Mountpoint: "/mnt/dataset1", Fstype: "ZFS"},
{Device: "tank/dataset2", Mountpoint: "/mnt/dataset2", Fstype: "ZFS"},
}, nil
}
diskUsage = func(ctx context.Context, path string) (*godisk.UsageStat, error) {
return &godisk.UsageStat{Total: 1000, Used: 500, Free: 500}, nil
}
// Mock queryZpoolStats to fail
origQueryZpoolStats := queryZpoolStats
defer func() { queryZpoolStats = origQueryZpoolStats }()
queryZpoolStats = func(ctx context.Context, pools []string) (map[string]zpoolStats, error) {
return nil, assert.AnError
}
ctx := context.Background()
disks := collectDisks(ctx)
// If case-sensitive check fails, it will return 2 disks
// If we fix it, it should return 1
assert.Len(t, disks, 1)
}
func TestCollectDisks_ZFS_Fuse(t *testing.T) {
// Save original functions
origDiskPartitions := diskPartitions
origDiskUsage := diskUsage
defer func() {
diskPartitions = origDiskPartitions
diskUsage = origDiskUsage
}()
// Mock partitions with "fuse.zfs"
diskPartitions = func(ctx context.Context, all bool) ([]godisk.PartitionStat, error) {
return []godisk.PartitionStat{
{Device: "tank/dataset1", Mountpoint: "/mnt/dataset1", Fstype: "fuse.zfs"},
{Device: "tank/dataset2", Mountpoint: "/mnt/dataset2", Fstype: "fuse.zfs"},
}, nil
}
diskUsage = func(ctx context.Context, path string) (*godisk.UsageStat, error) {
return &godisk.UsageStat{Total: 1000, Used: 500, Free: 500}, nil
}
// Mock queryZpoolStats to fail
origQueryZpoolStats := queryZpoolStats
defer func() { queryZpoolStats = origQueryZpoolStats }()
queryZpoolStats = func(ctx context.Context, pools []string) (map[string]zpoolStats, error) {
return nil, assert.AnError
}
ctx := context.Background()
disks := collectDisks(ctx)
// If check fails, it will return 2 disks
assert.Len(t, disks, 1)
}