mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-29 20:10:21 +00:00
292 lines
9.3 KiB
Go
292 lines
9.3 KiB
Go
package proxmox
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
)
|
|
|
|
func TestDiskUnmarshalWearout(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
wearout json.RawMessage
|
|
expected int
|
|
}{
|
|
{
|
|
name: "numeric",
|
|
wearout: json.RawMessage(`81`),
|
|
expected: 81,
|
|
},
|
|
{
|
|
name: "numeric string",
|
|
wearout: json.RawMessage(`"81"`),
|
|
expected: 81,
|
|
},
|
|
{
|
|
name: "escaped numeric string",
|
|
wearout: json.RawMessage(`"\"81\""`),
|
|
expected: 81,
|
|
},
|
|
{
|
|
name: "percentage string",
|
|
wearout: json.RawMessage(`"81%"`),
|
|
expected: 81,
|
|
},
|
|
{
|
|
name: "percentage with spaces",
|
|
wearout: json.RawMessage(`" 82 % "`),
|
|
expected: 82,
|
|
},
|
|
{
|
|
name: "not applicable string",
|
|
wearout: json.RawMessage(`"N/A"`),
|
|
expected: wearoutUnknown,
|
|
},
|
|
{
|
|
name: "empty string",
|
|
wearout: json.RawMessage(`""`),
|
|
expected: wearoutUnknown,
|
|
},
|
|
{
|
|
name: "null value",
|
|
wearout: json.RawMessage(`null`),
|
|
expected: wearoutUnknown,
|
|
},
|
|
{
|
|
name: "unknown string",
|
|
wearout: json.RawMessage(`"Unknown"`),
|
|
expected: wearoutUnknown,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
payload := fmt.Sprintf(`{"devpath":"/dev/sda","model":"Example","serial":"123","type":"hdd","health":"OK","wearout":%s,"size":1000,"rpm":7200,"used":"LVM","vendor":"Example","wwn":"example"}`, tc.wearout)
|
|
var disk Disk
|
|
if err := json.Unmarshal([]byte(payload), &disk); err != nil {
|
|
t.Fatalf("unexpected error unmarshalling disk: %v", err)
|
|
}
|
|
if disk.Wearout != tc.expected {
|
|
t.Fatalf("wearout: got %d, want %d", disk.Wearout, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVMFileSystemUnmarshalFlexibleNumbers(t *testing.T) {
|
|
t.Run("accepts numeric values", func(t *testing.T) {
|
|
payload := `{"name":"rootfs","type":"zfs","mountpoint":"/","total-bytes":8589934592,"used-bytes":3221225472,"disk":[{"dev":"/dev/vtbd0p2"}]}`
|
|
var fs VMFileSystem
|
|
if err := json.Unmarshal([]byte(payload), &fs); err != nil {
|
|
t.Fatalf("unexpected error unmarshalling numeric fields: %v", err)
|
|
}
|
|
if fs.TotalBytes != 8589934592 || fs.UsedBytes != 3221225472 {
|
|
t.Fatalf("unexpected numeric values: got total=%d used=%d", fs.TotalBytes, fs.UsedBytes)
|
|
}
|
|
})
|
|
|
|
t.Run("accepts numeric strings", func(t *testing.T) {
|
|
payload := `{"name":"rootfs","type":"ufs","mountpoint":"/","total-bytes":"5368709120","used-bytes":"2147483648","disk":[{"dev":"/dev/vtbd0p3"}]}`
|
|
var fs VMFileSystem
|
|
if err := json.Unmarshal([]byte(payload), &fs); err != nil {
|
|
t.Fatalf("unexpected error unmarshalling string fields: %v", err)
|
|
}
|
|
if fs.TotalBytes != 5368709120 || fs.UsedBytes != 2147483648 {
|
|
t.Fatalf("unexpected string values: got total=%d used=%d", fs.TotalBytes, fs.UsedBytes)
|
|
}
|
|
})
|
|
|
|
t.Run("accepts float-like strings", func(t *testing.T) {
|
|
payload := `{"name":"rootfs","type":"ufs","mountpoint":"/","total-bytes":"1073741824.0","used-bytes":"536870912.0","disk":[{"dev":"/dev/vtbd0p4"}]}`
|
|
var fs VMFileSystem
|
|
if err := json.Unmarshal([]byte(payload), &fs); err != nil {
|
|
t.Fatalf("unexpected error unmarshalling float-like strings: %v", err)
|
|
}
|
|
if fs.TotalBytes != 1073741824 || fs.UsedBytes != 536870912 {
|
|
t.Fatalf("unexpected float string values: got total=%d used=%d", fs.TotalBytes, fs.UsedBytes)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMemoryStatusEffectiveAvailable(t *testing.T) {
|
|
t.Run("nil receiver returns zero", func(t *testing.T) {
|
|
var status *MemoryStatus
|
|
if status.EffectiveAvailable() != 0 {
|
|
t.Fatalf("expected nil receiver to return 0")
|
|
}
|
|
})
|
|
|
|
tests := []struct {
|
|
name string
|
|
status MemoryStatus
|
|
want uint64
|
|
}{
|
|
{
|
|
name: "uses available field when set",
|
|
status: MemoryStatus{Total: 16 * 1024, Available: 6 * 1024},
|
|
want: 6 * 1024,
|
|
},
|
|
{
|
|
name: "uses avail field fallback",
|
|
status: MemoryStatus{Total: 16 * 1024, Avail: 5 * 1024},
|
|
want: 5 * 1024,
|
|
},
|
|
{
|
|
name: "derives from free buffers cached",
|
|
status: MemoryStatus{Total: 32 * 1024, Free: 4 * 1024, Buffers: 2 * 1024, Cached: 6 * 1024},
|
|
want: 12 * 1024,
|
|
},
|
|
{
|
|
name: "available zero but buffers and cache present",
|
|
status: MemoryStatus{
|
|
Total: 64 * 1024,
|
|
Used: 40 * 1024,
|
|
Free: 6 * 1024,
|
|
Buffers: 8 * 1024,
|
|
Cached: 10 * 1024,
|
|
},
|
|
want: 24 * 1024,
|
|
},
|
|
{
|
|
name: "caps derived value at total",
|
|
status: MemoryStatus{Total: 8 * 1024, Free: 4 * 1024, Buffers: 4 * 1024, Cached: 4 * 1024},
|
|
want: 8 * 1024,
|
|
},
|
|
{
|
|
name: "returns zero when no data",
|
|
status: MemoryStatus{Total: 24 * 1024},
|
|
want: 0,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := tc.status.EffectiveAvailable(); got != tc.want {
|
|
t.Fatalf("EffectiveAvailable: got %d, want %d", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMemoryStatusEffectiveAvailable_RegressionIssue435 tests the specific scenarios
|
|
// reported in GitHub issue #435 where memory calculations incorrectly included cache/buffers
|
|
func TestMemoryStatusEffectiveAvailable_RegressionIssue435(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
status MemoryStatus
|
|
wantAvailable uint64
|
|
wantUsedPct float64 // Expected usage percentage when using EffectiveAvailable
|
|
description string
|
|
}{
|
|
{
|
|
name: "real proxmox 8.x node with available field",
|
|
status: MemoryStatus{
|
|
Total: 16466186240, // 16GB
|
|
Used: 13981696000, // ~14GB (includes cache)
|
|
Free: 1558323200, // ~1.5GB
|
|
Available: 7422545920, // ~7GB (cache-aware - this is what we should use!)
|
|
Buffers: 23592960, // ~23MB
|
|
Cached: 5478891520, // ~5.4GB
|
|
},
|
|
wantAvailable: 7422545920,
|
|
wantUsedPct: 54.92, // Should be ~55%, not ~85%!
|
|
description: "Proxmox 8.x returns 'available' field - most accurate",
|
|
},
|
|
{
|
|
name: "older proxmox with avail field",
|
|
status: MemoryStatus{
|
|
Total: 8589934592, // 8GB
|
|
Used: 6871947674, // ~6.4GB (includes cache)
|
|
Free: 805306368, // ~768MB
|
|
Avail: 3221225472, // ~3GB (cache-aware)
|
|
},
|
|
wantAvailable: 3221225472,
|
|
wantUsedPct: 62.5, // Should be ~62.5%, not ~80%
|
|
description: "Older Proxmox uses 'avail' field as fallback",
|
|
},
|
|
{
|
|
name: "proxmox without available/avail - derive from components",
|
|
status: MemoryStatus{
|
|
Total: 16777216000, // ~16GB
|
|
Used: 13421772800, // ~12.5GB (includes cache)
|
|
Free: 2147483648, // 2GB
|
|
Buffers: 536870912, // 512MB
|
|
Cached: 4294967296, // 4GB
|
|
},
|
|
wantAvailable: 6979321856, // Free + Buffers + Cached = ~6.5GB
|
|
wantUsedPct: 58.4, // Should be ~58%, not ~80%
|
|
description: "When available/avail missing, derive from free+buffers+cached",
|
|
},
|
|
{
|
|
name: "proxmox 8.4 hides cache fields - derive from total-minus-used gap",
|
|
status: MemoryStatus{
|
|
Total: 134794743808, // ~125.6GB
|
|
Used: 107351023616, // ~100GB actual usage
|
|
Free: 6471057408, // ~6GB bare free reported
|
|
Buffers: 0,
|
|
Cached: 0,
|
|
},
|
|
wantAvailable: 27443720192, // total - used => ~25.6GB reclaimable (free + cache)
|
|
wantUsedPct: 79.6, // Matches Proxmox node dashboard
|
|
description: "Proxmox 8.4 stops reporting buffers/cached; use total-used gap to recover cache-aware metric",
|
|
},
|
|
{
|
|
name: "issue #435 specific case - 86% vs 42% real usage",
|
|
status: MemoryStatus{
|
|
Total: 33554432000, // ~32GB
|
|
Used: 28857589760, // ~27GB (includes cache - WRONG!)
|
|
Free: 1073741824, // 1GB
|
|
Available: 19327352832, // ~18GB (cache-aware - CORRECT!)
|
|
},
|
|
wantAvailable: 19327352832,
|
|
wantUsedPct: 42.4, // Should be ~42%, not 86%!
|
|
description: "Real user report: 86% shown when actual usage is 42%",
|
|
},
|
|
{
|
|
name: "missing available but buffers/cached still present (issue 553 guard)",
|
|
status: MemoryStatus{
|
|
Total: 34359738368, // 32GB
|
|
Used: 27917287424, // ~26GB reported used (includes cache)
|
|
Free: 2147483648, // 2GB
|
|
Buffers: 3221225472, // 3GB
|
|
Cached: 8053063680, // 7.5GB
|
|
},
|
|
wantAvailable: 13421772800, // Free + Buffers + Cached
|
|
wantUsedPct: 60.94,
|
|
description: "When available=0 but buffers/cached exist, derive reclaimable memory instead of alerting",
|
|
},
|
|
{
|
|
name: "missing all cache-aware fields - fallback to zero",
|
|
status: MemoryStatus{
|
|
Total: 8589934592,
|
|
Used: 6871947674,
|
|
Free: 0, // All fields missing
|
|
},
|
|
wantAvailable: 1717986918, // Derived from total - used
|
|
wantUsedPct: 80.0, // Still aligns with cache-inclusive calculation when nothing else reported
|
|
description: "When all cache fields missing, fall back to total-used gap instead of zero",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := tc.status.EffectiveAvailable()
|
|
if got != tc.wantAvailable {
|
|
t.Errorf("EffectiveAvailable() = %d, want %d\nScenario: %s",
|
|
got, tc.wantAvailable, tc.description)
|
|
}
|
|
|
|
// Calculate what the usage percentage would be with cache-aware calculation
|
|
if tc.wantAvailable > 0 && tc.wantAvailable <= tc.status.Total {
|
|
actualUsed := tc.status.Total - tc.wantAvailable
|
|
usagePct := (float64(actualUsed) / float64(tc.status.Total)) * 100
|
|
|
|
// Allow 0.5% tolerance for floating point differences
|
|
if usagePct < tc.wantUsedPct-0.5 || usagePct > tc.wantUsedPct+0.5 {
|
|
t.Errorf("Calculated usage = %.2f%%, want ~%.2f%%\nScenario: %s",
|
|
usagePct, tc.wantUsedPct, tc.description)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|