Expand monitoring and discovery test coverage
This commit is contained in:
parent
3a4fc044ea
commit
6fdef61710
4 changed files with 369 additions and 2 deletions
280
internal/monitoring/helpers_test.go
Normal file
280
internal/monitoring/helpers_test.go
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
package monitoring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizeEndpointHost(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{" node.local ", "node.local"},
|
||||||
|
{"https://node.local:8006/", "node.local"},
|
||||||
|
{"node.local:8006", "node.local"},
|
||||||
|
{"node.local/path", "node.local"},
|
||||||
|
{"https://[2001:db8::1]:8006", "2001:db8::1"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.input, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := normalizeEndpointHost(tc.input); got != tc.want {
|
||||||
|
t.Fatalf("normalizeEndpointHost(%q) = %q, want %q", tc.input, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsLikelyIPAddress(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
value string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"", false},
|
||||||
|
{"example.local", false},
|
||||||
|
{"10.0.0.1", true},
|
||||||
|
{"2001:db8::1", true},
|
||||||
|
{"fe80::1%eth0", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.value, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := isLikelyIPAddress(tc.value); got != tc.want {
|
||||||
|
t.Fatalf("isLikelyIPAddress(%q) = %v, want %v", tc.value, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNodeDisplayName(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
clusterInstance := &config.PVEInstance{
|
||||||
|
IsCluster: true,
|
||||||
|
Name: "cluster",
|
||||||
|
ClusterEndpoints: []config.ClusterEndpoint{
|
||||||
|
{NodeName: "node1", Host: "https://node1.local:8006"},
|
||||||
|
{NodeName: "node2", Host: "", IP: "10.0.0.2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
instance *config.PVEInstance
|
||||||
|
node string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"nil instance trims", nil, " nodeX ", "nodeX"},
|
||||||
|
{"friendly standalone", &config.PVEInstance{Name: "Friendly"}, "nodeA", "Friendly"},
|
||||||
|
{"host fallback", &config.PVEInstance{Host: "https://host.local:8006"}, "unknown-node", "host.local"},
|
||||||
|
{"cluster host label", clusterInstance, "node1", "node1.local"},
|
||||||
|
{"cluster ip fallback", clusterInstance, "node2", "node2"},
|
||||||
|
{"cluster base fallback", clusterInstance, "node3", "node3"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := getNodeDisplayName(tc.instance, tc.node); got != tc.want {
|
||||||
|
t.Fatalf("getNodeDisplayName(%v, %q) = %q, want %q", tc.instance, tc.node, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeNVMeTempsIntoDisks(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
original := []models.PhysicalDisk{
|
||||||
|
{Node: "nodeA", Instance: "inst", DevPath: "/dev/nvme1n1", Type: "nvme", Temperature: 55},
|
||||||
|
{Node: "nodeA", Instance: "inst", DevPath: "/dev/nvme0n1", Type: "NVME", Temperature: 60},
|
||||||
|
{Node: "nodeB", Instance: "inst", DevPath: "/dev/sda", Type: "sata", Temperature: 45},
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes := []models.Node{
|
||||||
|
{
|
||||||
|
Name: "nodeA",
|
||||||
|
Temperature: &models.Temperature{
|
||||||
|
Available: true,
|
||||||
|
NVMe: []models.NVMeTemp{
|
||||||
|
{Device: "nvme0n1", Temp: 30.4},
|
||||||
|
{Device: "nvme1n1", Temp: 31.6},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
merged := mergeNVMeTempsIntoDisks(original, nodes)
|
||||||
|
|
||||||
|
if got, want := merged[0].Temperature, 32; got != want {
|
||||||
|
t.Fatalf("disk 0 temperature = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
if got, want := merged[1].Temperature, 30; got != want {
|
||||||
|
t.Fatalf("disk 1 temperature = %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
if got, want := merged[2].Temperature, 45; got != want {
|
||||||
|
t.Fatalf("non-nvme disk temperature changed: got %d want %d", got, want)
|
||||||
|
}
|
||||||
|
if got := original[0].Temperature; got != 55 {
|
||||||
|
t.Fatalf("expected original slice unchanged, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeNVMeTempsIntoDisksClearsMissingOrInvalid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
disks := []models.PhysicalDisk{
|
||||||
|
{Node: "nodeA", Instance: "inst", DevPath: "/dev/nvme0n1", Type: "nvme", Temperature: 65},
|
||||||
|
{Node: "nodeC", Instance: "inst", DevPath: "/dev/nvme1n1", Type: "nvme", Temperature: 70},
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes := []models.Node{
|
||||||
|
{
|
||||||
|
Name: "nodeA",
|
||||||
|
Temperature: &models.Temperature{
|
||||||
|
Available: true,
|
||||||
|
NVMe: []models.NVMeTemp{
|
||||||
|
{Device: "nvme0n1", Temp: math.NaN()},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
merged := mergeNVMeTempsIntoDisks(disks, nodes)
|
||||||
|
|
||||||
|
if got := merged[0].Temperature; got != 0 {
|
||||||
|
t.Fatalf("expected NaN temp to reset to 0, got %d", got)
|
||||||
|
}
|
||||||
|
if got := merged[1].Temperature; got != 0 {
|
||||||
|
t.Fatalf("expected missing temps to reset to 0, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSafePercentage(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
used, total float64
|
||||||
|
want float64
|
||||||
|
}{
|
||||||
|
{50, 100, 50},
|
||||||
|
{0, 0, 0},
|
||||||
|
{math.NaN(), 100, 0},
|
||||||
|
{75, math.NaN(), 0},
|
||||||
|
{10, 0, 0},
|
||||||
|
{math.Inf(1), 100, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(fmt.Sprintf("%v/%v", tc.used, tc.total), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := safePercentage(tc.used, tc.total); got != tc.want {
|
||||||
|
t.Fatalf("safePercentage(%v, %v) = %v, want %v", tc.used, tc.total, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSafeFloat(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if got := safeFloat(math.NaN()); got != 0 {
|
||||||
|
t.Fatalf("expected NaN to return 0, got %v", got)
|
||||||
|
}
|
||||||
|
if got := safeFloat(math.Inf(1)); got != 0 {
|
||||||
|
t.Fatalf("expected +Inf to return 0, got %v", got)
|
||||||
|
}
|
||||||
|
if got := safeFloat(42.5); got != 42.5 {
|
||||||
|
t.Fatalf("expected value preserved, got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaxInt64(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if got := maxInt64(5, 10); got != 10 {
|
||||||
|
t.Fatalf("expected 10, got %d", got)
|
||||||
|
}
|
||||||
|
if got := maxInt64(-1, -5); got != -1 {
|
||||||
|
t.Fatalf("expected -1, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertPoolInfoToModel(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
info := proxmox.ZFSPoolInfo{
|
||||||
|
Name: "tank",
|
||||||
|
Health: "ONLINE",
|
||||||
|
State: "ONLINE",
|
||||||
|
Status: "OK",
|
||||||
|
Scan: "none requested",
|
||||||
|
Devices: []proxmox.ZFSPoolDevice{
|
||||||
|
{
|
||||||
|
Name: "mirror-0",
|
||||||
|
State: "ONLINE",
|
||||||
|
Leaf: 0,
|
||||||
|
Children: []proxmox.ZFSPoolDevice{
|
||||||
|
{
|
||||||
|
Name: "nvme0n1",
|
||||||
|
State: "ONLINE",
|
||||||
|
Leaf: 1,
|
||||||
|
Read: 1,
|
||||||
|
Write: 2,
|
||||||
|
Cksum: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "nvme1n1",
|
||||||
|
State: "ONLINE",
|
||||||
|
Leaf: 1,
|
||||||
|
Read: 4,
|
||||||
|
Write: 5,
|
||||||
|
Cksum: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
model := convertPoolInfoToModel(&info)
|
||||||
|
if model == nil {
|
||||||
|
t.Fatalf("expected pool model, got nil")
|
||||||
|
}
|
||||||
|
if model.Name != "tank" {
|
||||||
|
t.Fatalf("expected pool name tank, got %s", model.Name)
|
||||||
|
}
|
||||||
|
if model.State != "ONLINE" {
|
||||||
|
t.Fatalf("expected ONLINE state, got %s", model.State)
|
||||||
|
}
|
||||||
|
if len(model.Devices) != 2 {
|
||||||
|
t.Fatalf("expected 2 leaf devices, got %d", len(model.Devices))
|
||||||
|
}
|
||||||
|
if model.ReadErrors != 5 || model.WriteErrors != 7 || model.ChecksumErrors != 9 {
|
||||||
|
t.Fatalf("unexpected error totals: read=%d write=%d checksum=%d", model.ReadErrors, model.WriteErrors, model.ChecksumErrors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertPoolInfoToModelNil(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if model := convertPoolInfoToModel(nil); model != nil {
|
||||||
|
t.Fatalf("expected nil result for nil input")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -191,12 +191,10 @@ func (s *Scanner) scanWorker(ctx context.Context, wg *sync.WaitGroup, ipChan <-c
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
// Check port 8006 (could be PVE or PMG)
|
|
||||||
if server := s.checkPort8006(ctx, ip); server != nil {
|
if server := s.checkPort8006(ctx, ip); server != nil {
|
||||||
resultChan <- server
|
resultChan <- server
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check PBS (port 8007)
|
|
||||||
if server := s.checkServer(ctx, ip, 8007, "pbs"); server != nil {
|
if server := s.checkServer(ctx, ip, 8007, "pbs"); server != nil {
|
||||||
resultChan <- server
|
resultChan <- server
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -377,3 +377,22 @@ func TestDiscoverServersWithCallback(t *testing.T) {
|
||||||
t.Fatalf("expected callbacks for both servers, got %d", callbackCount)
|
t.Fatalf("expected callbacks for both servers, got %d", callbackCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDiscoverServersCancelledContext(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
scanner := NewScanner()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
result, err := scanner.DiscoverServersWithCallback(ctx, "127.0.0.1/32", nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected context error, got nil")
|
||||||
|
}
|
||||||
|
if result == nil {
|
||||||
|
t.Fatalf("expected result object even on cancellation")
|
||||||
|
}
|
||||||
|
if len(result.Servers) != 0 {
|
||||||
|
t.Fatalf("expected no servers on cancelled context")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
70
pkg/proxmox/zfs_test.go
Normal file
70
pkg/proxmox/zfs_test.go
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
package proxmox
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestConvertDeviceRecursiveClassifiesVdevs(t *testing.T) {
|
||||||
|
mirror := ZFSPoolDevice{
|
||||||
|
Name: "mirror-0",
|
||||||
|
State: "degraded",
|
||||||
|
Leaf: 0,
|
||||||
|
Children: []ZFSPoolDevice{
|
||||||
|
{Name: "sda", State: "ONLINE", Leaf: 1},
|
||||||
|
{Name: "sdb", State: "ONLINE", Leaf: 1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
devices := convertDeviceRecursive(mirror, "")
|
||||||
|
if len(devices) != 1 {
|
||||||
|
t.Fatalf("expected single vdev entry, got %d", len(devices))
|
||||||
|
}
|
||||||
|
vdev := devices[0]
|
||||||
|
if vdev.Type != "mirror" {
|
||||||
|
t.Fatalf("expected mirror type, got %s", vdev.Type)
|
||||||
|
}
|
||||||
|
if vdev.State != "DEGRADED" {
|
||||||
|
t.Fatalf("expected DEGRADED state, got %s", vdev.State)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertDeviceRecursiveLeavesIncludeErrors(t *testing.T) {
|
||||||
|
dev := ZFSPoolDevice{
|
||||||
|
Name: "nvme0n1",
|
||||||
|
State: "degRaDed",
|
||||||
|
Leaf: 1,
|
||||||
|
Read: 1,
|
||||||
|
Write: 2,
|
||||||
|
Cksum: 3,
|
||||||
|
Msg: "device error",
|
||||||
|
}
|
||||||
|
|
||||||
|
devices := convertDeviceRecursive(dev, "")
|
||||||
|
if len(devices) != 1 {
|
||||||
|
t.Fatalf("expected single device, got %d", len(devices))
|
||||||
|
}
|
||||||
|
leaf := devices[0]
|
||||||
|
if !leaf.IsLeaf {
|
||||||
|
t.Fatalf("expected leaf device")
|
||||||
|
}
|
||||||
|
if leaf.Type != "disk" {
|
||||||
|
t.Fatalf("expected disk type, got %s", leaf.Type)
|
||||||
|
}
|
||||||
|
if leaf.ReadErrors != 1 || leaf.WriteErrors != 2 || leaf.ChecksumErrors != 3 {
|
||||||
|
t.Fatalf("unexpected error counts: %+v", leaf)
|
||||||
|
}
|
||||||
|
if leaf.Message != "device error" {
|
||||||
|
t.Fatalf("expected message to propagate, got %q", leaf.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertDeviceRecursiveSkipsHealthyLeaf(t *testing.T) {
|
||||||
|
dev := ZFSPoolDevice{
|
||||||
|
Name: "nvme0n1",
|
||||||
|
State: "ONLINE",
|
||||||
|
Leaf: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
devices := convertDeviceRecursive(dev, "")
|
||||||
|
if len(devices) != 0 {
|
||||||
|
t.Fatalf("expected healthy leaf to be omitted, got %d entries", len(devices))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue